diff --git a/spec/parser/javascript_spec.js b/spec/parser/javascript_spec.js deleted file mode 100644 index b0620f4..0000000 --- a/spec/parser/javascript_spec.js +++ /dev/null @@ -1,28 +0,0 @@ -import parser from 'src/js/parser/javascript.js'; - -describe('parser/javascript.peg', function() { - - describe('regular expression literals', function() { - - describe('flags support', function() { - - it('handles the ignore case flag', function() { - expect(parser.parse('/test/i').flags().ignore_case).toEqual(true); - expect(parser.parse('/test/').flags().ignore_case).toEqual(false); - }); - - it('handles the global flag', function() { - expect(parser.parse('/test/g').flags().global).toEqual(true); - expect(parser.parse('/test/').flags().global).toEqual(false); - }); - - it('handles the multiline flag', function() { - expect(parser.parse('/test/m').flags().multiline).toEqual(true); - expect(parser.parse('/test/').flags().multiline).toEqual(false); - }); - - }); - - }); - -}); diff --git a/spec/regexper_spec.js b/spec/regexper_spec.js new file mode 100644 index 0000000..f6a2cb5 --- /dev/null +++ b/spec/regexper_spec.js @@ -0,0 +1,333 @@ +import Regexper from 'src/js/regexper.js'; +import Q from 'q'; + +describe('regexper.js', function() { + + beforeEach(function() { + this.root = document.createElement('div'); + this.root.innerHTML = [ + '
', + '
', + '
', + '
', + '
', + '
' + ].join(''); + + this.regexper = new Regexper(this.root); + spyOn(this.regexper, '_setHash'); + spyOn(this.regexper, '_getHash').and.returnValue('example hash value'); + }); + + describe('#keypressListener', function() { + + beforeEach(function() { + this.event = document.createEvent('Event'); + spyOn(this.event, 'preventDefault'); + spyOn(this.regexper.form, 'dispatchEvent'); + }); + + describe('when the shift key is not depressed', function() { + + beforeEach(function() { + this.event.shiftKey = false; + this.event.keyCode = 13; + }); + + it('does not prevent the default action', function() { + this.regexper.keypressListener(this.event); + expect(this.event.returnValue).not.toEqual(false); + expect(this.event.preventDefault).not.toHaveBeenCalled(); + }); + + it('does not trigger a submit event', function() { + this.regexper.keypressListener(this.event); + expect(this.regexper.form.dispatchEvent).not.toHaveBeenCalled(); + }); + + }); + + describe('when the keyCode is not 13 (Enter)', function() { + + beforeEach(function() { + this.event.shiftKey = true; + this.event.keyCode = 42; + }); + + it('does not prevent the default action', function() { + this.regexper.keypressListener(this.event); + expect(this.event.returnValue).not.toEqual(false); + expect(this.event.preventDefault).not.toHaveBeenCalled(); + }); + + it('does not trigger a submit event', function() { + this.regexper.keypressListener(this.event); + expect(this.regexper.form.dispatchEvent).not.toHaveBeenCalled(); + }); + + }); + + describe('when the shift key is depressed and the keyCode is 13 (Enter)', function() { + + beforeEach(function() { + this.event.shiftKey = true; + this.event.keyCode = 13; + }); + + it('prevents the default action', function() { + this.regexper.keypressListener(this.event); + expect(this.event.returnValue).not.toEqual(true); + expect(this.event.preventDefault).toHaveBeenCalled(); + }); + + it('triggers a submit event', function() { + var event; + + this.regexper.keypressListener(this.event); + expect(this.regexper.form.dispatchEvent).toHaveBeenCalled(); + + event = this.regexper.form.dispatchEvent.calls.mostRecent().args[0]; + expect(event.type).toEqual('submit'); + }); + + }); + + }); + + describe('#submitListener', function() { + + beforeEach(function() { + this.event = document.createEvent('Event'); + spyOn(this.event, 'preventDefault'); + + this.regexper.field.value = 'example value'; + }); + + it('prevents the default action', function() { + this.regexper.submitListener(this.event); + expect(this.event.returnValue).not.toEqual(true); + expect(this.event.preventDefault).toHaveBeenCalled(); + }); + + it('sets the location.hash', function() { + this.regexper.submitListener(this.event); + expect(this.regexper._setHash).toHaveBeenCalledWith('example value'); + }); + + describe('when setting location.hash fails', function() { + + beforeEach(function() { + this.regexper._setHash.and.throwError('hash failure'); + }); + + it('disables the permalink', function() { + this.regexper.submitListener(this.event); + expect(this.regexper.permalinkEnabled).toEqual(false); + }); + + it('shows the expression directly', function() { + spyOn(this.regexper, 'showExpression'); + this.regexper.submitListener(this.event); + expect(this.regexper.showExpression).toHaveBeenCalledWith('example value'); + }); + + }); + + }); + + describe('#hashchangeListener', function() { + + it('enables the permalink', function() { + this.regexper.hashchangeListener(); + expect(this.regexper.permalinkEnabled).toEqual(true); + }); + + it('shows the expression from the hash', function() { + spyOn(this.regexper, 'showExpression'); + this.regexper.hashchangeListener(); + expect(this.regexper.showExpression).toHaveBeenCalledWith('example hash value'); + }); + + }); + + describe('#updatePercentage', function() { + + beforeEach(function() { + this.event = document.createEvent('Event'); + this.event.detail = { percentage: 0.42 }; + }); + + it('sets the width of the progress bar', function() { + this.regexper.updatePercentage(this.event); + expect(this.regexper.percentage.style.width).toEqual('42%'); + }); + + }); + + describe('#bindListeners', function() { + + it('binds #keypressListener to keypress on the text field', function() { + spyOn(this.regexper.field, 'addEventListener'); + spyOn(this.regexper, 'keypressListener'); + this.regexper.bindListeners(); + expect(this.regexper.field.addEventListener).toHaveBeenCalledWith('keypress', jasmine.any(Function)); + + this.regexper.field.addEventListener.calls.mostRecent().args[1](); + expect(this.regexper.keypressListener).toHaveBeenCalled(); + }); + + it('binds #submitListener to submit on the form', function() { + spyOn(this.regexper.form, 'addEventListener'); + spyOn(this.regexper, 'submitListener'); + this.regexper.bindListeners(); + expect(this.regexper.form.addEventListener).toHaveBeenCalledWith('submit', jasmine.any(Function)); + + this.regexper.form.addEventListener.calls.mostRecent().args[1](); + expect(this.regexper.submitListener).toHaveBeenCalled(); + }); + + it('binds #updatePercentage to updateStatus on the root', function() { + spyOn(this.regexper.root, 'addEventListener'); + spyOn(this.regexper, 'updatePercentage'); + this.regexper.bindListeners(); + expect(this.regexper.root.addEventListener).toHaveBeenCalledWith('updateStatus', jasmine.any(Function)); + + this.regexper.root.addEventListener.calls.mostRecent().args[1](); + expect(this.regexper.updatePercentage).toHaveBeenCalled(); + }); + + it('binds #hashchangeListener to hashchange on the window', function() { + spyOn(window, 'addEventListener'); + spyOn(this.regexper, 'hashchangeListener'); + this.regexper.bindListeners(); + expect(window.addEventListener).toHaveBeenCalledWith('hashchange', jasmine.any(Function)); + + window.addEventListener.calls.mostRecent().args[1](); + expect(this.regexper.hashchangeListener).toHaveBeenCalled(); + }); + + }); + + describe('#showExpression', function() { + + beforeEach(function() { + this.renderPromise = Q.defer(); + spyOn(this.regexper, 'renderRegexp').and.returnValue(this.renderPromise.promise); + }); + + it('sets the text field value', function() { + this.regexper.showExpression('example expression'); + expect(this.regexper.field.value).toEqual('example expression'); + }); + + describe('when the expression is blank', function() { + + it('clears the state', function() { + this.regexper.showExpression(''); + expect(this.regexper.state).toEqual(''); + }); + + }); + + describe('when the expression is not blank', function() { + + it('sets the state to "is-loading"', function() { + this.regexper.showExpression('example expression'); + expect(this.regexper.state).toEqual('is-loading'); + }); + + it('renders the expression', function() { + this.regexper.showExpression('example expression'); + expect(this.regexper.renderRegexp).toHaveBeenCalledWith('example expression'); + }); + + describe('when the expression finishes rendering', function() { + + beforeEach(function(done) { + spyOn(this.regexper, 'updateLinks'); + this.regexper.showExpression('example expression'); + this.renderPromise.resolve(); + setTimeout(done, 100); + }); + + it('sets the state to "has-results"', function() { + expect(this.regexper.state).toEqual('has-results'); + }); + + it('updates the links', function() { + expect(this.regexper.updateLinks).toHaveBeenCalled(); + }); + + }); + + }); + + }); + + describe('#updateLinks', function() { + + beforeEach(function() { + spyOn(this.regexper, 'buildBlobURL'); + }); + + describe('when blob URLs are supported', function() { + + beforeEach(function() { + this.regexper.buildBlobURL.and.returnValue('http://example.com/blob'); + }); + + it('sets the download link href', function() { + this.regexper.updateLinks(); + expect(this.regexper.download.href).toEqual('http://example.com/blob'); + }); + + }); + + describe('when blob URLs are not supported', function() { + + beforeEach(function() { + this.regexper.buildBlobURL.and.throwError('blob failure'); + }); + + it('hides the download link', function() { + this.regexper.updateLinks(); + expect(this.regexper.download.parentNode.style.display).toEqual('none'); + }); + + }); + + describe('when the permalink is enabled', function() { + + beforeEach(function() { + this.regexper.permalinkEnabled = true; + }); + + it('sets the permalink href', function() { + this.regexper.updateLinks(); + expect(this.regexper.permalink.href).toEqual(location.toString()); + }); + + }); + + describe('when the permalink is disabled', function() { + + beforeEach(function() { + this.regexper.permalinkEnabled = false; + }); + + it('hides the permalink', function() { + this.regexper.updateLinks(); + expect(this.regexper.permalink.parentNode.style.display).toEqual('none'); + }); + + }); + + }); + + describe('#renderRegexp', function() { + + + + }); + +}); diff --git a/src/js/regexper.js b/src/js/regexper.js index 04db34a..9d96629 100644 --- a/src/js/regexper.js +++ b/src/js/regexper.js @@ -33,40 +33,25 @@ export default class Regexper { } submitListener(event) { - event.preventDefault(); + event.returnValue = false; + if (event.preventDefault) { + event.preventDefault(); + } try { - this.disablePermalink = false; - location.hash = this.field.value; + this._setHash(this.field.value); } catch(e) { // Most likely failed to set the URL has (probably because the expression // is too long). Turn off the permalink and just show the expression - this.disablePermalink = true; + this.permalinkEnabled = false; this.showExpression(this.field.value); } } hashchangeListener() { - var expression = decodeURIComponent(location.hash.slice(1)); - - this.showExpression(expression); - } - - showExpression(expression) { - this.field.value = expression; - this.state = ''; - - if (expression !== '') { - this.state = 'is-loading'; - - this.renderRegexp(expression.replace(/\n/g, '\\n')) - .then(() => { - this.state = 'has-results'; - this.updateLinks(); - }) - .done(); - } + this.permalinkEnabled = true; + this.showExpression(this._getHash()); } updatePercentage(event) { @@ -80,27 +65,50 @@ export default class Regexper { window.addEventListener('hashchange', this.hashchangeListener.bind(this)); } + _setHash(hash) { + location.hash = encodeURIComponent(hash); + } + + _getHash() { + return decodeURIComponent(location.hash.slice(1)); + } + set state(state) { this.root.className = state; } - showError(message) { - this.state = 'has-error'; - this.error.innerHTML = ''; - this.error.appendChild(document.createTextNode(message)); + get state() { + return this.root.className; + } - throw message; + showExpression(expression) { + this.field.value = expression; + this.state = ''; + + if (expression !== '') { + this.state = 'is-loading'; + + this.renderRegexp(expression) + .then(() => { + this.state = 'has-results'; + this.updateLinks(); + }) + .done(); + } + } + + buildBlobURL(content) { + blob = new Blob([content], { type: 'image/svg+xml' }); + window.blob = blob; // Blob object has to stick around for IE + return URL.createObjectURL(blob); } updateLinks() { var blob, url; try { - blob = new Blob([this.svg.parentNode.innerHTML], { type: 'image/svg+xml' }); - url = URL.createObjectURL(blob); - window.blob = blob; // Blob object has to stick around for IE - - this.download.setAttribute('href', url); + this.download.parentNode.style.display = null; + this.download.href = this.buildBlobURL(this.svg.parentNode.innerHTML); } catch(e) { // Blobs or URLs created from them don't work here. @@ -108,11 +116,11 @@ export default class Regexper { this.download.parentNode.style.display = 'none'; } - if (this.disablePermalink) { - this.permalink.parentNode.style.display = 'none'; - } else { + if (this.permalinkEnabled) { this.permalink.parentNode.style.display = null; - this.permalink.setAttribute('href', location.toString()); + this.permalink.href = location.toString(); + } else { + this.permalink.parentNode.style.display = 'none'; } } @@ -123,8 +131,14 @@ export default class Regexper { parser.resetGroupCounter(); - return Q.fcall(parser.parse.bind(parser), expression) - .then(null, this.showError.bind(this)) + return Q.fcall(parser.parse.bind(parser), expression.replace(/\n/g, '\\n')) + .then(null, message => { + this.state = 'has-error'; + this.error.innerHTML = ''; + this.error.appendChild(document.createTextNode(message)); + + throw message; + }) .invoke('render', snap.group()) .then(result => { var box;