diff options
| -rw-r--r-- | lib/nodeserver/server.js | 34 | ||||
| -rw-r--r-- | src/scenario/Application.js | 66 | ||||
| -rw-r--r-- | src/scenario/Scenario.js | 1 | ||||
| -rw-r--r-- | src/scenario/dsl.js | 4 | ||||
| -rw-r--r-- | test/scenario/ApplicationSpec.js | 106 |
5 files changed, 181 insertions, 30 deletions
diff --git a/lib/nodeserver/server.js b/lib/nodeserver/server.js index 08f13a99..f91f6afa 100644 --- a/lib/nodeserver/server.js +++ b/lib/nodeserver/server.js @@ -8,10 +8,8 @@ var DEFAULT_PORT = 8000; function main(argv) { new HttpServer({ - 'GET': (function() { - var servlet = new StaticServlet(); - return servlet.handleRequest.bind(servlet) - })() + 'GET': createServlet(StaticServlet), + 'HEAD': createServlet(StaticServlet) }).start(Number(argv[2]) || DEFAULT_PORT); } @@ -22,6 +20,11 @@ function escapeHtml(value) { replace('"', '"'); } +function createServlet(Class) { + var servlet = new Class(); + return servlet.handleRequest.bind(servlet); +} + /** * An Http server implementation that uses a map of methods to decide * action routing. @@ -61,11 +64,10 @@ HttpServer.prototype.handleRequest_ = function(req, res) { } }; - /** * Handles static content. */ -function StaticServlet() {} +function StaticServlet() {} StaticServlet.MimeMap = { 'txt': 'text/plain', @@ -164,13 +166,17 @@ StaticServlet.prototype.sendFile_ = function(req, res, path) { 'Content-Type': StaticServlet. MimeMap[path.split('.').pop()] || 'text/plain' }); - file.on('data', res.write.bind(res)); - file.on('close', function() { + if (req.method === 'HEAD') { res.end(); - }); - file.on('error', function(error) { - self.sendError_(req, res, error); - }); + } else { + file.on('data', res.write.bind(res)); + file.on('close', function() { + res.end(); + }); + file.on('error', function(error) { + self.sendError_(req, res, error); + }); + } }; StaticServlet.prototype.sendDirectory_ = function(req, res, path) { @@ -207,6 +213,10 @@ StaticServlet.prototype.writeDirectoryIndex_ = function(req, res, path, files) { res.writeHead(200, { 'Content-Type': 'text/html' }); + if (req.method === 'HEAD') { + res.end(); + return; + } res.write('<!doctype html>\n'); res.write('<title>' + escapeHtml(path) + '</title>\n'); res.write('<style>\n'); diff --git a/src/scenario/Application.js b/src/scenario/Application.js index e2d34551..eacf3c7b 100644 --- a/src/scenario/Application.js +++ b/src/scenario/Application.js @@ -38,27 +38,70 @@ angular.scenario.Application.prototype.getWindow_ = function() { }; /** + * Checks that a URL would return a 2xx success status code. Callback is called + * with no arguments on success, or with an error on failure. + * + * Warning: This requires the server to be able to respond to HEAD requests + * and not modify the state of your application. + * + * @param {string} url Url to check + * @param {Function} callback function(error) that is called with result. + */ +angular.scenario.Application.prototype.checkUrlStatus_ = function(url, callback) { + var self = this; + _jQuery.ajax({ + url: url, + type: 'HEAD', + complete: function(request) { + if (request.status < 200 || request.status >= 300) { + if (!request.status) { + callback.call(self, 'Sandbox Error: Cannot access ' + url); + } else { + callback.call(self, request.status + ' ' + request.statusText); + } + } else { + callback.call(self); + } + } + }); +}; + +/** * Changes the location of the frame. * * @param {string} url The URL. If it begins with a # then only the * hash of the page is changed. - * @param {Function} onloadFn function($window, $document) + * @param {Function} loadFn function($window, $document) Called when frame loads. + * @param {Function} errorFn function(error) Called if any error when loading. */ -angular.scenario.Application.prototype.navigateTo = function(url, onloadFn) { +angular.scenario.Application.prototype.navigateTo = function(url, loadFn, errorFn) { var self = this; var frame = this.getFrame_(); - if (url.charAt(0) === '#') { + //TODO(esprehn): Refactor to use rethrow() + errorFn = errorFn || function(e) { throw e; }; + if (/^file:\/\//.test(url)) { + errorFn('Sandbox Error: Cannot load file:// URL.'); + } else if (url.charAt(0) === '#') { url = frame.attr('src').split('#')[0] + url; frame.attr('src', url); - this.executeAction(onloadFn); + this.executeAction(loadFn); } else { frame.css('display', 'none').attr('src', 'about:blank'); - this.context.find('#test-frames').append('<iframe>'); - frame = this.getFrame_(); - frame.load(function() { - self.executeAction(onloadFn); - frame.unbind(); - }).attr('src', url); + this.checkUrlStatus_(url, function(error) { + if (error) { + return errorFn(error); + } + self.context.find('#test-frames').append('<iframe>'); + frame = this.getFrame_(); + frame.load(function() { + frame.unbind(); + try { + self.executeAction(loadFn); + } catch (e) { + errorFn(e); + } + }).attr('src', url); + }); } this.context.find('> h2 a').attr('href', url).text(url); }; @@ -73,6 +116,9 @@ angular.scenario.Application.prototype.navigateTo = function(url, onloadFn) { angular.scenario.Application.prototype.executeAction = function(action) { var self = this; var $window = this.getWindow_(); + if (!$window.document) { + throw 'Sandbox Error: Application document not accessible.'; + } if (!$window.angular) { return action.call(this, $window, _jQuery($window.document)); } diff --git a/src/scenario/Scenario.js b/src/scenario/Scenario.js index 14d530ac..d141c42b 100644 --- a/src/scenario/Scenario.js +++ b/src/scenario/Scenario.js @@ -1,3 +1,4 @@ + /** * Setup file for the Scenario. * Must be first in the compilation/bootstrap list. diff --git a/src/scenario/dsl.js b/src/scenario/dsl.js index fcc2e5b2..358f1fce 100644 --- a/src/scenario/dsl.js +++ b/src/scenario/dsl.js @@ -62,7 +62,7 @@ angular.scenario.dsl('navigateTo', function() { } application.navigateTo(url, function() { done(null, url); - }); + }, done); }); }; }); @@ -271,7 +271,7 @@ angular.scenario.dsl('element', function() { if (href && elements[0].nodeName.toUpperCase() === 'A') { this.application.navigateTo(href, function() { done(); - }); + }, done); } else { done(); } diff --git a/test/scenario/ApplicationSpec.js b/test/scenario/ApplicationSpec.js index 3e1c862d..75a7e9c9 100644 --- a/test/scenario/ApplicationSpec.js +++ b/test/scenario/ApplicationSpec.js @@ -1,15 +1,24 @@ describe('angular.scenario.Application', function() { var app, frames; + function callLoadHandlers(app) { + var handlers = app.getFrame_().data('events').load; + expect(handlers).toBeDefined(); + expect(handlers.length).toEqual(1); + handlers[0].handler(); + } + beforeEach(function() { frames = _jQuery("<div></div>"); app = new angular.scenario.Application(frames); + app.checkUrlStatus_ = function(url, callback) { + callback.call(this); + }; }); it('should return new $window and $document after navigate', function() { var called; var testWindow, testDocument, counter = 0; - app.navigateTo = noop; app.getWindow_ = function() { return {x:counter++, document:{x:counter++}}; }; @@ -50,6 +59,31 @@ describe('angular.scenario.Application', function() { expect(app.getFrame_().attr('test')).toBeFalsy(); }); + it('should call error handler if document not accessible', function() { + app.getWindow_ = function() { + return {}; + }; + app.navigateTo('about:blank', angular.noop, function(error) { + expect(error).toMatch(/Sandbox Error/); + }); + callLoadHandlers(app); + }); + + it('should call error handler if using file:// URL', function() { + app.navigateTo('file://foo/bar.txt', angular.noop, function(error) { + expect(error).toMatch(/Sandbox Error/); + }); + }); + + it('should call error handler if status check fails', function() { + app.checkUrlStatus_ = function(url, callback) { + callback.call(this, 'Example Error'); + }; + app.navigateTo('about:blank', angular.noop, function(error) { + expect(error).toEqual('Example Error'); + }); + }); + it('should hide old iframes and navigate to about:blank', function() { app.navigateTo('about:blank#foo'); app.navigateTo('about:blank#bar'); @@ -70,15 +104,12 @@ describe('angular.scenario.Application', function() { it('should call onload handler when frame loads', function() { var called; app.getWindow_ = function() { - return {}; + return {document: {}}; }; app.navigateTo('about:blank', function($window, $document) { called = true; }); - var handlers = app.getFrame_().data('events').load; - expect(handlers).toBeDefined(); - expect(handlers.length).toEqual(1); - handlers[0].handler(); + callLoadHandlers(app); expect(called).toBeTruthy(); }); @@ -113,4 +144,67 @@ describe('angular.scenario.Application', function() { expect(handlers.length).toEqual(1); handlers[0](); }); + + describe('jQuery ajax', function() { + var options; + var response; + var jQueryAjax; + + beforeEach(function() { + response = { + status: 200, + statusText: 'OK' + }; + jQueryAjax = _jQuery.ajax; + _jQuery.ajax = function(opts) { + options = opts; + opts.complete.call(this, response); + }; + app.checkUrlStatus_ = angular.scenario.Application. + prototype.checkUrlStatus_; + }); + + afterEach(function() { + _jQuery.ajax = jQueryAjax; + }); + + it('should perform a HEAD request to verify file existence', function() { + app.navigateTo('http://www.google.com/', angular.noop, angular.noop); + expect(options.type).toEqual('HEAD'); + expect(options.url).toEqual('http://www.google.com/'); + }); + + it('should call error handler if status code is less than 200', function() { + var finished; + response.status = 199; + response.statusText = 'Error Message'; + app.navigateTo('about:blank', angular.noop, function(error) { + expect(error).toEqual('199 Error Message'); + finished = true; + }); + expect(finished).toBeTruthy(); + }); + + it('should call error handler if status code is greater than 299', function() { + var finished; + response.status = 300; + response.statusText = 'Error'; + app.navigateTo('about:blank', angular.noop, function(error) { + expect(error).toEqual('300 Error'); + finished = true; + }); + expect(finished).toBeTruthy(); + }); + + it('should call error handler if status code is 0 for sandbox error', function() { + var finished; + response.status = 0; + response.statusText = ''; + app.navigateTo('about:blank', angular.noop, function(error) { + expect(error).toEqual('Sandbox Error: Cannot access about:blank'); + finished = true; + }); + expect(finished).toBeTruthy(); + }); + }); }); |
