diff --git a/.travis.yml b/.travis.yml index 56718cfcfc..56ba281685 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: node_js node_js: - stable -script: npm run lint && npm run build +script: npm run lint && npm run test && npm run build addons: sauce_connect: username: "chriseth" diff --git a/package.json b/package.json index 07fa048f07..ee3d46f471 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "description": "Minimalistic browser-based Solidity IDE", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "node test/index.js", "build": "mkdir -p build; browserify src/index.js -o build/app.js", "lint": "semistandard" }, @@ -19,6 +19,7 @@ "jquery": "^2.2.0", "js-base64": "^2.1.9", "semistandard": "^7.0.0", + "tape": "^4.5.1", "web3": "^0.15.3", "webworkify": "^1.2.1" }, diff --git a/src/app.js b/src/app.js index 5297cdbd28..d8aaeba334 100644 --- a/src/app.js +++ b/src/app.js @@ -3,12 +3,16 @@ var $ = require('jquery'); var utils = require('./app/utils'); -var queryParams = require('./app/query-params'); -var gistHandler = require('./app/gist-handler'); +var QueryParams = require('./app/query-params'); +var queryParams = new QueryParams(); +var GistHandler = require('./app/gist-handler'); +var gistHandler = new GistHandler(); var StorageHandler = require('./app/storage-handler'); var Editor = require('./app/editor'); +var Renderer = require('./app/renderer'); var Compiler = require('./app/compiler'); +var ExecutionContext = require('./app/execution-context'); // The event listener needs to be registered as early as possible, because the // parent will send the message upon the "load" event. @@ -61,7 +65,7 @@ var run = function () { // ------------------ gist load ---------------- - var loadingFromGist = gistHandler.handleLoad(function (gistId) { + var loadingFromGist = gistHandler.handleLoad(queryParams.get(), function (gistId) { $.ajax({ url: 'https://api.github.com/gists/' + gistId, jsonp: 'callback', @@ -421,7 +425,10 @@ var run = function () { return $.getJSON('https://api.github.com/repos/' + root + '/contents/' + path, cb); } - var compiler = new Compiler(editor, handleGithubCall, $('#output'), getHidingRHP, updateFiles); + var executionContext = new ExecutionContext(); + var renderer = new Renderer(editor, executionContext, updateFiles); + var compiler = new Compiler(editor, renderer, queryParams, handleGithubCall, $('#output'), getHidingRHP, updateFiles); + executionContext.setCompiler(compiler); function setVersionText (text) { $('#version').text(text); diff --git a/src/app/compiler.js b/src/app/compiler.js index b47dfbebe7..0121136a4d 100644 --- a/src/app/compiler.js +++ b/src/app/compiler.js @@ -1,18 +1,13 @@ var webworkify = require('webworkify'); -var queryParams = require('./query-params'); var utils = require('./utils'); -var Renderer = require('./renderer'); var Base64 = require('js-base64').Base64; -function Compiler (editor, handleGithubCall, outputField, hidingRHP, updateFiles) { - var renderer = new Renderer(editor, this, updateFiles); - +function Compiler (editor, renderer, queryParams, handleGithubCall, outputField, hidingRHP, updateFiles) { var compileJSON; var compilerAcceptsMultipleFiles; var previousInput = ''; - var sourceAnnotations = []; var cachedRemoteFiles = {}; var worker = null; @@ -39,7 +34,6 @@ function Compiler (editor, handleGithubCall, outputField, hidingRHP, updateFiles var compile = function (missingInputs) { editor.clearAnnotations(); - sourceAnnotations = []; outputField.empty(); var input = editor.getValue(); editor.setCacheFileContent(input); @@ -58,10 +52,10 @@ function Compiler (editor, handleGithubCall, outputField, hidingRHP, updateFiles }; this.compile = compile; - this.addAnnotation = function (annotation) { - sourceAnnotations[sourceAnnotations.length] = annotation; - editor.setAnnotations(sourceAnnotations); - }; + function setCompileJSON (_compileJSON) { + compileJSON = _compileJSON; + } + this.setCompileJSON = setCompileJSON; // this is exposed for testing function onCompilerLoaded (setVersionText, version) { setVersionText(version); @@ -91,14 +85,14 @@ function Compiler (editor, handleGithubCall, outputField, hidingRHP, updateFiles compilerAcceptsMultipleFiles = false; compile = Module.cwrap('compileJSON', 'string', [ 'string', 'number' ]); } - compileJSON = function (source, optimize, cb) { + setCompileJSON(function (source, optimize, cb) { try { var result = compile(source, optimize); } catch (exception) { result = JSON.stringify({ error: 'Uncaught JavaScript exception:\n' + exception }); } compilationFinished(result, missingInputs); - }; + }); onCompilerLoaded(setVersionText, Module.cwrap('version', 'string', [])()); } } @@ -149,7 +143,7 @@ function Compiler (editor, handleGithubCall, outputField, hidingRHP, updateFiles function loadInternal (url, setVersionText) { delete window.Module; // Set a safe fallback until the new one is loaded - compileJSON = function (source, optimize) { compilationFinished('{}'); }; + setCompileJSON(function (source, optimize) { compilationFinished('{}'); }); var newScript = document.createElement('script'); newScript.type = 'text/javascript'; @@ -183,9 +177,9 @@ function Compiler (editor, handleGithubCall, outputField, hidingRHP, updateFiles }); worker.onerror = function (msg) { console.log(msg.data); }; worker.addEventListener('error', function (msg) { console.log(msg.data); }); - compileJSON = function (source, optimize) { + setCompileJSON(function (source, optimize) { worker.postMessage({cmd: 'compile', source: source, optimize: optimize}); - }; + }); worker.postMessage({cmd: 'loadVersion', data: url}); } diff --git a/src/app/editor.js b/src/app/editor.js index 7742b0fb08..e8dc95f906 100644 --- a/src/app/editor.js +++ b/src/app/editor.js @@ -6,6 +6,15 @@ var ace = require('brace'); require('../mode-solidity.js'); function Editor (loadingFromGist) { + var SOL_CACHE_UNTITLED = utils.getCacheFilePrefix() + 'Untitled'; + var SOL_CACHE_FILE = null; + + var editor = ace.edit('input'); + var sessions = {}; + var sourceAnnotations = []; + + setupStuff(getFiles()); + this.newFile = function () { var untitledCount = ''; while (window.localStorage[SOL_CACHE_UNTITLED + untitledCount]) { @@ -58,7 +67,7 @@ function Editor (loadingFromGist) { return this.getFiles().indexOf(utils.fileKey(name)) !== -1; }; - this.getFiles = function () { + function getFiles () { var files = []; for (var f in window.localStorage) { if (f.indexOf(utils.getCacheFilePrefix(), 0) === 0) { @@ -67,7 +76,8 @@ function Editor (loadingFromGist) { } } return files; - }; + } + this.getFiles = getFiles; this.packageFiles = function () { var files = {}; @@ -100,9 +110,15 @@ function Editor (loadingFromGist) { }; this.clearAnnotations = function () { + sourceAnnotations = []; editor.getSession().clearAnnotations(); }; + this.addAnnotation = function (annotation) { + sourceAnnotations[sourceAnnotations.length] = annotation; + this.setAnnotations(sourceAnnotations); + }; + this.setAnnotations = function (sourceAnnotations) { editor.getSession().setAnnotations(sourceAnnotations); }; @@ -152,14 +168,6 @@ function Editor (loadingFromGist) { editor.setSession(sessions[SOL_CACHE_FILE]); editor.resize(true); } - - var SOL_CACHE_UNTITLED = utils.getCacheFilePrefix() + 'Untitled'; - var SOL_CACHE_FILE = null; - - var editor = ace.edit('input'); - var sessions = {}; - - setupStuff(this.getFiles()); } module.exports = Editor; diff --git a/src/app/execution-context.js b/src/app/execution-context.js index 4627a8634e..db67897a80 100644 --- a/src/app/execution-context.js +++ b/src/app/execution-context.js @@ -13,9 +13,14 @@ if (typeof window.web3 !== 'undefined') { web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545')); } -function ExecutionContext (compiler) { +function ExecutionContext () { + var compiler; var executionContext = injectedProvider ? 'injected' : 'vm'; + this.setCompiler = function (_compiler) { + compiler = _compiler; + }; + this.isVM = function () { return executionContext === 'vm'; }; diff --git a/src/app/gist-handler.js b/src/app/gist-handler.js index 7d07651512..b69cbac4fc 100644 --- a/src/app/gist-handler.js +++ b/src/app/gist-handler.js @@ -1,14 +1,12 @@ -/* global prompt */ +// Allowing window to be overriden for testing +function GistHandler (_window) { + if (_window === undefined) _window = window; -var queryParams = require('./query-params'); - -function handleLoad (cb) { - var params = queryParams.get(); - var loadingFromGist = false; - if (typeof params['gist'] !== undefined) { + this.handleLoad = function (params, cb) { + var loadingFromGist = false; var gistId; if (params['gist'] === '') { - var str = prompt('Enter the URL or ID of the Gist you would like to load.'); + var str = _window.prompt('Enter the URL or ID of the Gist you would like to load.'); if (str !== '') { gistId = getGistId(str); loadingFromGist = !!gistId; @@ -20,16 +18,14 @@ function handleLoad (cb) { if (loadingFromGist) { cb(gistId); } - } - return loadingFromGist; -} + return loadingFromGist; + }; -function getGistId (str) { - var idr = /[0-9A-Fa-f]{8,}/; - var match = idr.exec(str); - return match ? match[0] : null; + function getGistId (str) { + var idr = /[0-9A-Fa-f]{8,}/; + var match = idr.exec(str); + return match ? match[0] : null; + } } -module.exports = { - handleLoad: handleLoad -}; +module.exports = GistHandler; diff --git a/src/app/query-params.js b/src/app/query-params.js index 391a242195..0b11eac34f 100644 --- a/src/app/query-params.js +++ b/src/app/query-params.js @@ -1,38 +1,40 @@ -function getQueryParams () { - var qs = window.location.hash.substr(1); +// Allowing window to be overriden for testing +function QueryParams (_window) { + if (_window === undefined) _window = window; - if (window.location.search.length > 0) { - // use legacy query params instead of hash - window.location.hash = window.location.search.substr(1); - window.location.search = ''; - } + this.get = function () { + var qs = _window.location.hash.substr(1); - var params = {}; - var parts = qs.split('&'); - for (var x in parts) { - var keyValue = parts[x].split('='); - if (keyValue[0] !== '') { - params[keyValue[0]] = keyValue[1]; + if (_window.location.search.length > 0) { + // use legacy query params instead of hash + _window.location.hash = _window.location.search.substr(1); + _window.location.search = ''; } - } - return params; -} -function updateQueryParams (params) { - var currentParams = getQueryParams(); - var keys = Object.keys(params); - for (var x in keys) { - currentParams[keys[x]] = params[keys[x]]; - } - var queryString = '#'; - var updatedKeys = Object.keys(currentParams); - for (var y in updatedKeys) { - queryString += updatedKeys[y] + '=' + currentParams[updatedKeys[y]] + '&'; - } - window.location.hash = queryString.slice(0, -1); + var params = {}; + var parts = qs.split('&'); + for (var x in parts) { + var keyValue = parts[x].split('='); + if (keyValue[0] !== '') { + params[keyValue[0]] = keyValue[1]; + } + } + return params; + }; + + this.update = function (params) { + var currentParams = this.get(); + var keys = Object.keys(params); + for (var x in keys) { + currentParams[keys[x]] = params[keys[x]]; + } + var queryString = '#'; + var updatedKeys = Object.keys(currentParams); + for (var y in updatedKeys) { + queryString += updatedKeys[y] + '=' + currentParams[updatedKeys[y]] + '&'; + } + _window.location.hash = queryString.slice(0, -1); + }; } -module.exports = { - get: getQueryParams, - update: updateQueryParams -}; +module.exports = QueryParams; diff --git a/src/app/renderer.js b/src/app/renderer.js index 811ec9b44d..21229e68f0 100644 --- a/src/app/renderer.js +++ b/src/app/renderer.js @@ -3,11 +3,9 @@ var $ = require('jquery'); var UniversalDApp = require('../universal-dapp.js'); var utils = require('./utils'); -var ExecutionContext = require('./execution-context'); -function Renderer (editor, compiler, updateFiles) { +function Renderer (editor, executionContext, updateFiles) { var detailsOpen = {}; - var executionContext = new ExecutionContext(compiler); function renderError (message) { var type = utils.errortype(message); @@ -20,7 +18,7 @@ function Renderer (editor, compiler, updateFiles) { var errLine = parseInt(err[2], 10) - 1; var errCol = err[4] ? parseInt(err[4], 10) : 0; if (errFile === '' || errFile === utils.fileNameFromKey(editor.getCacheFile())) { - compiler.addAnnotation({ + editor.addAnnotation({ row: errLine, column: errCol, text: message, diff --git a/test/compiler-test.js b/test/compiler-test.js new file mode 100644 index 0000000000..80f803208e --- /dev/null +++ b/test/compiler-test.js @@ -0,0 +1,17 @@ +var test = require('tape'); + +var Compiler = require('../src/app/compiler'); + +test('compiler.compile smoke', function (t) { + t.plan(1); + + var noop = function () {}; + var getCacheFile = function () { return 'fakeCacheFile'; }; + var fakeEditor = {onChangeSetup: noop, clearAnnotations: noop, getValue: noop, setCacheFileContent: noop, getCacheFile: getCacheFile}; + var fakeOutputField = {empty: noop}; + var fakeQueryParams = {get: function () { return {}; }}; + var compiler = new Compiler(fakeEditor, null, fakeQueryParams, null, fakeOutputField); + compiler.setCompileJSON(noop); + compiler.compile(); + t.ok(compiler); +}); diff --git a/test/gist-handler-test.js b/test/gist-handler-test.js new file mode 100644 index 0000000000..d260310aac --- /dev/null +++ b/test/gist-handler-test.js @@ -0,0 +1,69 @@ +var test = require('tape'); + +var GistHandler = require('../src/app/gist-handler'); + +test('gistHandler.handleLoad with no gist param', function (t) { + t.plan(1); + + var gistHandler = new GistHandler({}); + + var params = {}; + var result = gistHandler.handleLoad(params, null); + + t.equal(result, false); +}); + +test('gistHandler.handleLoad with blank gist param, and invalid user input', function (t) { + t.plan(3); + + var fakeWindow = {prompt: function (message) { + t.ok(message); + t.ok(message.match(/gist/i)); + return 'invalid'; + }}; + + var gistHandler = new GistHandler(fakeWindow); + + var params = {'gist': ''}; + var result = gistHandler.handleLoad(params, null); + + t.equal(result, false); +}); + +test('gistHandler.handleLoad with blank gist param, and valid user input', function (t) { + t.plan(4); + + var fakeWindow = {prompt: function (message) { + t.ok(message); + t.ok(message.match(/gist/i)); + return 'Beef1234'; + }}; + + var cb = function (gistId) { + t.equal(gistId, 'Beef1234'); + }; + + var gistHandler = new GistHandler(fakeWindow); + + var params = {'gist': ''}; + var result = gistHandler.handleLoad(params, cb); + + t.equal(result, true); +}); + +test('gistHandler.handleLoad with gist param', function (t) { + t.plan(2); + + var gistHandler = new GistHandler({}); + + var params = {'gist': 'abc'}; + + var cb = function (gistId) { + t.equal(gistId, 'abc'); + }; + + var result = gistHandler.handleLoad(params, cb); + + t.equal(result, true); +}); + diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000000..db15b90e82 --- /dev/null +++ b/test/index.js @@ -0,0 +1,3 @@ +require('./compiler-test'); +require('./gist-handler-test'); +require('./query-params-test'); diff --git a/test/query-params-test.js b/test/query-params-test.js new file mode 100644 index 0000000000..e3cff5c546 --- /dev/null +++ b/test/query-params-test.js @@ -0,0 +1,21 @@ +var test = require('tape'); + +var QueryParams = require('../src/app/query-params'); + +test('queryParams.get', function (t) { + t.plan(2); + + var fakeWindow = {location: {hash: '#wat=sup&foo=bar', search: ''}}; + var params = new QueryParams(fakeWindow).get(); + t.equal(params.wat, 'sup'); + t.equal(params.foo, 'bar'); +}); + +test('queryParams.update', function (t) { + t.plan(1); + + var fakeWindow = {location: {hash: '#wat=sup', search: ''}}; + var qp = new QueryParams(fakeWindow); + qp.update({foo: 'bar'}); + t.equal(fakeWindow.location.hash, '#wat=sup&foo=bar'); +});