diff --git a/modules/DOMUtils.js b/modules/DOMUtils.js new file mode 100644 index 0000000000..6325ac8969 --- /dev/null +++ b/modules/DOMUtils.js @@ -0,0 +1,75 @@ +var PathUtils = require('./PathUtils'); + +var STATE_KEY_QUERY_PARAM = '_sk'; + +function getHashPath() { + return decodeURI( + // We can't use window.location.hash here because it's not + // consistent across browsers - Firefox will pre-decode it! + window.location.href.split('#')[1] || '' + ); +} + +function getWindowPath() { + return decodeURI( + window.location.pathname + window.location.search + ); +} + +function getState(path) { + var stateID = getStateID(path); + var serializedState = stateID && window.sessionStorage.getItem(stateID); + return serializedState ? JSON.parse(serializedState) : null; +} + +function getStateID(path) { + var query = PathUtils.extractQuery(path); + return query && query[STATE_KEY_QUERY_PARAM]; +} + +function withStateID(path, stateID) { + var query = Path.extractQuery(path) || {}; + query[STATE_KEY_QUERY_PARAM] = stateID; + return PathUtils.withQuery(PathUtils.withoutQuery(path), query); +} + +function withoutStateID(path) { + var query = PathUtils.extractQuery(path); + + if (STATE_KEY_QUERY_PARAM in query) { + delete query[STATE_KEY_QUERY_PARAM]; + return PathUtils.withQuery(PathUtils.withoutQuery(path), query); + } + + return path; +} + +function saveState(state) { + var stateID = state.id; + + if (stateID == null) + stateID = state.id = Math.random().toString(36).slice(2); + + window.sessionStorage.setItem( + stateID, + JSON.stringify(state) + ); + + return stateID; +} + +function withState(path, state) { + var stateID = state != null && saveState(state); + return stateID ? withStateID(path, stateID) : withoutStateID(path); +} + +module.exports = { + getHashPath, + getWindowPath, + getState, + getStateID, + withStateID, + withoutStateID, + saveState, + withState +}; diff --git a/modules/History.js b/modules/History.js deleted file mode 100644 index f9c758ec26..0000000000 --- a/modules/History.js +++ /dev/null @@ -1,31 +0,0 @@ -var invariant = require('react/lib/invariant'); -var canUseDOM = require('react/lib/ExecutionEnvironment').canUseDOM; - -var History = { - - /** - * The current number of entries in the history. - * - * Note: This property is read-only. - */ - length: 1, - - /** - * Sends the browser back one entry in the history. - */ - back: function () { - invariant( - canUseDOM, - 'Cannot use History.back without a DOM' - ); - - // Do this first so that History.length will - // be accurate in location change listeners. - History.length -= 1; - - window.history.back(); - } - -}; - -module.exports = History; diff --git a/modules/Location.js b/modules/Location.js new file mode 100644 index 0000000000..d53bb4518f --- /dev/null +++ b/modules/Location.js @@ -0,0 +1,10 @@ +class Location { + + constructor(path, state=null) { + this.path = path; + this.state = state; + } + +} + +module.exports = Location; diff --git a/modules/actions/LocationActions.js b/modules/LocationActions.js similarity index 100% rename from modules/actions/LocationActions.js rename to modules/LocationActions.js diff --git a/modules/history/DOMHistory.js b/modules/history/DOMHistory.js new file mode 100644 index 0000000000..3b7c448122 --- /dev/null +++ b/modules/history/DOMHistory.js @@ -0,0 +1,39 @@ +var invariant = require('react/lib/invariant'); +var History = require('./History'); + +class DOMHistory extends History { + + get length() { + var state = this.getCurrentState(); + return state && state.length || 1; + } + + get current() { + var state = this.getCurrentState(); + return state && state.current || this.length - 1; + } + + canGo(n) { + if (n === 0) + return true; + + var next = this.current + n; + return next >= 0 && next < this.length; + } + + go(n) { + if (n === 0) + return; + + invariant( + this.canGo(n), + 'Cannot go(%s); there is not enough history', + n + ); + + window.history.go(n); + } + +} + +module.exports = DOMHistory; diff --git a/modules/history/HTML5History.js b/modules/history/HTML5History.js new file mode 100644 index 0000000000..39bff34882 --- /dev/null +++ b/modules/history/HTML5History.js @@ -0,0 +1,69 @@ +/* jshint -W058 */ +var assign = require('react/lib/Object.assign'); +var LocationActions = require('../LocationActions'); +var { getWindowPath } = require('../DOMUtils'); +var DOMHistory = require('./DOMHistory'); + +var isListening = false; + +function onPopState(event) { + if (event.state === undefined) + return; // Ignore extraneous popstate events in WebKit. + + HTML5History.notifyChange(LocationActions.POP); +} + +/** + * A history implementation for DOM environments that support the + * HTML5 history API (pushState and replaceState). Provides the cleanest + * URLs. This implementation should always be used if possible. + */ +var HTML5History = assign(new DOMHistory, { + + addChangeListener(listener) { + DOMHistory.prototype.addChangeListener.call(this, listener); + + if (!isListening) { + if (window.addEventListener) { + window.addEventListener('popstate', onPopState, false); + } else { + window.attachEvent('onpopstate', onPopState); + } + + isListening = true; + } + }, + + removeChangeListener(listener) { + DOMHistory.prototype.removeChangeListener.call(this, listener); + + if (this.changeListeners.length === 0) { + if (window.addEventListener) { + window.removeEventListener('popstate', onPopState, false); + } else { + window.removeEvent('onpopstate', onPopState); + } + + isListening = false; + } + }, + + pushState(state, path) { + window.history.pushState(state, '', path); + this.notifyChange(LocationActions.PUSH); + }, + + replaceState(state, path) { + window.history.replaceState(state, '', path); + this.notifyChange(LocationActions.REPLACE); + }, + + getCurrentPath: getWindowPath, + + getCurrentState() { + return window.history.state; + } + +}); + +module.exports = HTML5History; diff --git a/modules/history/HashHistory.js b/modules/history/HashHistory.js new file mode 100644 index 0000000000..1898f23239 --- /dev/null +++ b/modules/history/HashHistory.js @@ -0,0 +1,92 @@ +/* jshint -W058 */ +var assign = require('react/lib/Object.assign'); +var LocationActions = require('../LocationActions'); +var { getHashPath, withState, withoutStateID, getState } = require('../DOMUtils'); +var DOMHistory = require('./DOMHistory'); + +var currentLocationAction; +var isListening = false; + +function ensureSlash() { + var path = HashHistory.getCurrentPath(); + + if (path.charAt(0) === '/') + return true; + + HashHistory.replace('/' + path); + + return false; +} + +function onHashChange() { + if (ensureSlash()) { + HashHistory.notifyChange( + currentLocationAction || LocationActions.POP + ); + + currentLocationAction = null; + } +} + +/** + * A history implementation for DOM environments that uses window.location.hash + * to store the current path. This is a hack for older browsers that do not + * support the HTML5 history API (IE <= 9). It is currently used as the + * default in DOM environments because it offers the widest range of support. + */ +var HashHistory = assign(new DOMHistory, { + + addChangeListener(listener) { + DOMHistory.prototype.addChangeListener.call(this, listener); + + // Do this BEFORE listening for hashchange. + ensureSlash(); + + if (!isListening) { + if (window.addEventListener) { + window.addEventListener('hashchange', onHashChange, false); + } else { + window.attachEvent('onhashchange', onHashChange); + } + + isListening = true; + } + }, + + removeChangeListener(listener) { + DOMHistory.prototype.removeChangeListener.call(this, listener); + + if (this.changeListeners.length === 0) { + if (window.removeEventListener) { + window.removeEventListener('hashchange', onHashChange, false); + } else { + window.removeEvent('onhashchange', onHashChange); + } + + isListening = false; + } + }, + + pushState(state, path) { + currentLocationAction = LocationActions.PUSH; + window.location.hash = withState(path, state); + }, + + replaceState(state, path) { + currentLocationAction = LocationActions.REPLACE; + window.location.replace( + window.location.pathname + window.location.search + '#' + withState(path, state) + ); + }, + + getCurrentPath() { + return withoutStateID(getHashPath()); + }, + + getCurrentState() { + return getState(getHashPath()); + } + +}); + +module.exports = HashHistory; diff --git a/modules/history/History.js b/modules/history/History.js new file mode 100644 index 0000000000..6d5c004c30 --- /dev/null +++ b/modules/history/History.js @@ -0,0 +1,68 @@ +var Location = require('../Location'); + +/** + * An abstract base class for history objects. Requires subclasses + * to implement the following properties/methods: + * + * - length + * - pushState(state, path) + * - replaceState(state, path) + * - getCurrentPath() + * - getCurrentState() + * - go(n) + */ +class History { + + addChangeListener(listener) { + if (!this.changeListeners) + this.changeListeners = []; + + this.changeListeners.push(listener); + } + + removeChangeListener(listener) { + if (!this.changeListeners) + return; + + this.changeListeners = this.changeListeners.filter(function (li) { + return li !== listener; + }); + } + + notifyChange(changeType) { + if (!this.changeListeners) + return; + + var location = this.getCurrentLocation(); + + for (var i = 0, len = this.changeListeners.length; i < len; ++i) + this.changeListeners[i].call(this, location, changeType); + } + + getCurrentLocation() { + return new Location(this.getCurrentPath(), this.getCurrentState()); + } + + canGo(n) { + return n === 0; + } + + canGoBack() { + return this.canGo(-1); + } + + canGoForward() { + return this.canGo(1); + } + + back() { + this.go(-1); + } + + forward() { + this.go(1); + } + +} + +module.exports = History; diff --git a/modules/history/RefreshHistory.js b/modules/history/RefreshHistory.js new file mode 100644 index 0000000000..192d04afb2 --- /dev/null +++ b/modules/history/RefreshHistory.js @@ -0,0 +1,31 @@ +/* jshint -W058 */ +var assign = require('react/lib/Object.assign'); +var { getWindowPath, withState, withoutStateID, getState } = require('../DOMUtils'); +var DOMHistory = require('./DOMHistory'); + +/** + * A history implementation that can be used in DOM environments + * that lack support for HTML5 history. Automatically used as the + * fallback when HTML5 history is desired but not available. + */ +var RefreshHistory = assign(new DOMHistory, { + + pushState(state, path) { + window.location = withState(path, state); + }, + + replaceState(state, path) { + window.location.replace(withState(path, state)); + }, + + getCurrentPath() { + return withoutStateID(getWindowPath()); + }, + + getCurrentState() { + return getState(getWindowPath()); + } + +}); + +module.exports = RefreshHistory; diff --git a/modules/history/StaticHistory.js b/modules/history/StaticHistory.js new file mode 100644 index 0000000000..c90b8f690d --- /dev/null +++ b/modules/history/StaticHistory.js @@ -0,0 +1,52 @@ +var invariant = require('react/lib/invariant'); +var History = require('./History'); + +function throwCannotModify() { + invariant(false, 'StaticHistory cannot be modified'); +} + +/** + * A history implementation that is convenient for stateless + * server environments where the URL is given once. Allows you + * to specify the path and state at creation time. + */ +class StaticHistory extends History { + + constructor(path, state=null) { + this.path = path; + this.state = state; + } + + get length() { + return 1; + } + + go(n) { + if (n === 0) + return; + + invariant( + false, + 'StaticHistory cannot go(%s); it cannot be modified', + n + ); + } + + getCurrentPath() { + return this.path; + } + + getCurrentState() { + return this.state; + } + +} + +// TODO: Include these in the above class definition +// once we can use ES7 property initializers. +// https://github.com/babel/babel/issues/619 + +StaticHistory.prototype.pushState = throwCannotModify; +StaticHistory.prototype.replaceState = throwCannotModify; + +module.exports = StaticHistory; diff --git a/modules/history/TestHistory.js b/modules/history/TestHistory.js new file mode 100644 index 0000000000..6657cb39e7 --- /dev/null +++ b/modules/history/TestHistory.js @@ -0,0 +1,82 @@ +var invariant = require('react/lib/invariant'); +var LocationActions = require('../LocationActions'); +var History = require('./History'); + +function createEntry(object) { + return typeof object === 'string' ? { path: object, state: null } : object; +} + +/** + * A history implementation that is convenient for testing. Allows + * you to specify all history entries at creation time, as well as + * the current index in the history to start from. + */ +class TestHistory extends History { + + /** + * If entries is given it must be an array of either paths (strings) + * or { path, state } objects. + */ + constructor(entries, startingIndex) { + this.entries = (entries || []).map(createEntry); + this.index = startingIndex || this.entries.length - 1; + } + + get length() { + return this.entries.length; + } + + canGo(n) { + if (n === 0) + return true; + + var nextIndex = this.index + n; + return nextIndex >= 0 && nextIndex < this.length; + } + + go(n) { + if (n === 0) + return; + + invariant( + this.canGo(n), + 'Cannot go(%s); there is not enough history', + n + ); + + this.index += n; + + this.notifyChange(LocationActions.POP); + } + + pushState(state, path) { + this.entries.push({ path, state }); + this.notifyChange(LocationActions.PUSH); + } + + replaceState(state, path) { + invariant( + this.length > 0, + 'You cannot use replaceState with no history' + ); + + this.entries[this.index] = { path, state }; + + this.notifyChange(LocationActions.REPLACE); + } + + getCurrentPath() { + return this.getCurrentEntry().path; + } + + getCurrentState() { + return this.getCurrentEntry().state; + } + + getCurrentEntry() { + return this.entries[this.index]; + } + +} + +module.exports = TestHistory; diff --git a/modules/history/__tests__/HTML5History-test.js b/modules/history/__tests__/HTML5History-test.js new file mode 100644 index 0000000000..64137ab465 --- /dev/null +++ b/modules/history/__tests__/HTML5History-test.js @@ -0,0 +1,6 @@ +var describeHistory = require('./describeHistory'); +var HTML5History = require('../HTML5History'); + +describe('HTML5History', function () { + describeHistory(HTML5History); +}); diff --git a/modules/history/__tests__/HashHistory-test.js b/modules/history/__tests__/HashHistory-test.js new file mode 100644 index 0000000000..16d802193f --- /dev/null +++ b/modules/history/__tests__/HashHistory-test.js @@ -0,0 +1,6 @@ +var describeHistory = require('./describeHistory'); +var HashHistory = require('../HashHistory'); + +describe('HashHistory', function () { + describeHistory(HashHistory); +}); diff --git a/modules/history/__tests__/RefreshHistory-test.js b/modules/history/__tests__/RefreshHistory-test.js new file mode 100644 index 0000000000..4ea1935170 --- /dev/null +++ b/modules/history/__tests__/RefreshHistory-test.js @@ -0,0 +1,6 @@ +var describeHistory = require('./describeHistory'); +var RefreshHistory = require('../RefreshHistory'); + +describe('RefreshHistory', function () { + describeHistory(RefreshHistory); +}); diff --git a/modules/history/__tests__/StaticHistory-test.js b/modules/history/__tests__/StaticHistory-test.js new file mode 100644 index 0000000000..b19f3a3ccc --- /dev/null +++ b/modules/history/__tests__/StaticHistory-test.js @@ -0,0 +1,6 @@ +var describeHistory = require('./describeHistory'); +var StaticHistory = require('../StaticHistory'); + +describe('StaticHistory', function () { + describeHistory(new StaticHistory()); +}); diff --git a/modules/history/__tests__/TestHistory-test.js b/modules/history/__tests__/TestHistory-test.js new file mode 100644 index 0000000000..0ae7176c38 --- /dev/null +++ b/modules/history/__tests__/TestHistory-test.js @@ -0,0 +1,6 @@ +var describeHistory = require('./describeHistory'); +var TestHistory = require('../TestHistory'); + +describe('TestHistory', function () { + describeHistory(new TestHistory()); +}); diff --git a/modules/history/__tests__/describeHistory.js b/modules/history/__tests__/describeHistory.js new file mode 100644 index 0000000000..5408aa43b1 --- /dev/null +++ b/modules/history/__tests__/describeHistory.js @@ -0,0 +1,29 @@ +var expect = require('expect'); +var History = require('../History'); + +function describeHistory(history) { + it('is an instanceof History', function () { + expect(history).toBeA(History); + }); + + // Check for required subclass properties. + it('has a numeric length property', function () { + expect(history.length).toBeA('number'); + }); + + // Check for required subclass methods. + [ 'pushState', + 'replaceState', + 'getCurrentPath', + 'getCurrentState', + 'go' + ].forEach(function (methodName) { + it(`has a ${methodName} method`, function () { + expect(history[methodName]).toBeA('function'); + }); + }); + + // TODO +} + +module.exports = describeHistory; diff --git a/modules/locations/HashLocation.js b/modules/locations/HashLocation.js deleted file mode 100644 index 7d67a81b51..0000000000 --- a/modules/locations/HashLocation.js +++ /dev/null @@ -1,113 +0,0 @@ -var LocationActions = require('../actions/LocationActions'); -var History = require('../History'); - -var _listeners = []; -var _isListening = false; -var _actionType; - -function notifyChange(type) { - if (type === LocationActions.PUSH) - History.length += 1; - - var change = { - path: HashLocation.getCurrentPath(), - type: type - }; - - _listeners.forEach(function (listener) { - listener.call(HashLocation, change); - }); -} - -function ensureSlash() { - var path = HashLocation.getCurrentPath(); - - if (path.charAt(0) === '/') - return true; - - HashLocation.replace('/' + path); - - return false; -} - -function onHashChange() { - if (ensureSlash()) { - // If we don't have an _actionType then all we know is the hash - // changed. It was probably caused by the user clicking the Back - // button, but may have also been the Forward button or manual - // manipulation. So just guess 'pop'. - notifyChange(_actionType || LocationActions.POP); - _actionType = null; - } -} - -/** - * A Location that uses `window.location.hash`. - */ -var HashLocation = { - - addChangeListener(listener) { - _listeners.push(listener); - - // Do this BEFORE listening for hashchange. - ensureSlash(); - - if (!_isListening) { - if (window.addEventListener) { - window.addEventListener('hashchange', onHashChange, false); - } else { - window.attachEvent('onhashchange', onHashChange); - } - - _isListening = true; - } - }, - - removeChangeListener(listener) { - _listeners = _listeners.filter(function (l) { - return l !== listener; - }); - - if (_listeners.length === 0) { - if (window.removeEventListener) { - window.removeEventListener('hashchange', onHashChange, false); - } else { - window.removeEvent('onhashchange', onHashChange); - } - - _isListening = false; - } - }, - - push(path) { - _actionType = LocationActions.PUSH; - window.location.hash = path; - }, - - replace(path) { - _actionType = LocationActions.REPLACE; - window.location.replace( - window.location.pathname + window.location.search + '#' + path - ); - }, - - pop() { - _actionType = LocationActions.POP; - History.back(); - }, - - getCurrentPath() { - return decodeURI( - // We can't use window.location.hash here because it's not - // consistent across browsers - Firefox will pre-decode it! - window.location.href.split('#')[1] || '' - ); - }, - - toString() { - return ''; - } - -}; - -module.exports = HashLocation; diff --git a/modules/locations/HistoryLocation.js b/modules/locations/HistoryLocation.js deleted file mode 100644 index d08dfa34ed..0000000000 --- a/modules/locations/HistoryLocation.js +++ /dev/null @@ -1,85 +0,0 @@ -var LocationActions = require('../actions/LocationActions'); -var History = require('../History'); - -var _listeners = []; -var _isListening = false; - -function notifyChange(type) { - var change = { - path: HistoryLocation.getCurrentPath(), - type: type - }; - - _listeners.forEach(function (listener) { - listener.call(HistoryLocation, change); - }); -} - -function onPopState(event) { - if (event.state === undefined) - return; // Ignore extraneous popstate events in WebKit. - - notifyChange(LocationActions.POP); -} - -/** - * A Location that uses HTML5 history. - */ -var HistoryLocation = { - - addChangeListener(listener) { - _listeners.push(listener); - - if (!_isListening) { - if (window.addEventListener) { - window.addEventListener('popstate', onPopState, false); - } else { - window.attachEvent('onpopstate', onPopState); - } - - _isListening = true; - } - }, - - removeChangeListener(listener) { - _listeners = _listeners.filter(function (l) { - return l !== listener; - }); - - if (_listeners.length === 0) { - if (window.addEventListener) { - window.removeEventListener('popstate', onPopState, false); - } else { - window.removeEvent('onpopstate', onPopState); - } - - _isListening = false; - } - }, - - push(path) { - window.history.pushState({ path: path }, '', path); - History.length += 1; - notifyChange(LocationActions.PUSH); - }, - - replace(path) { - window.history.replaceState({ path: path }, '', path); - notifyChange(LocationActions.REPLACE); - }, - - pop: History.back, - - getCurrentPath() { - return decodeURI( - window.location.pathname + window.location.search - ); - }, - - toString() { - return ''; - } - -}; - -module.exports = HistoryLocation; diff --git a/modules/locations/RefreshLocation.js b/modules/locations/RefreshLocation.js deleted file mode 100644 index 5706d49020..0000000000 --- a/modules/locations/RefreshLocation.js +++ /dev/null @@ -1,29 +0,0 @@ -var HistoryLocation = require('./HistoryLocation'); -var History = require('../History'); - -/** - * A Location that uses full page refreshes. This is used as - * the fallback for HistoryLocation in browsers that do not - * support the HTML5 history API. - */ -var RefreshLocation = { - - push(path) { - window.location = path; - }, - - replace(path) { - window.location.replace(path); - }, - - pop: History.back, - - getCurrentPath: HistoryLocation.getCurrentPath, - - toString() { - return ''; - } - -}; - -module.exports = RefreshLocation; diff --git a/modules/locations/StaticLocation.js b/modules/locations/StaticLocation.js deleted file mode 100644 index 49dcd6a426..0000000000 --- a/modules/locations/StaticLocation.js +++ /dev/null @@ -1,36 +0,0 @@ -var invariant = require('react/lib/invariant'); - -function throwCannotModify() { - invariant(false, 'You cannot modify a static location'); -} - -/** - * A location that only ever contains a single path. Useful in - * stateless environments like servers where there is no path history, - * only the path that was used in the request. - */ -class StaticLocation { - - constructor(path) { - this.path = path; - } - - getCurrentPath() { - return this.path; - } - - toString() { - return ``; - } - -} - -// TODO: Include these in the above class definition -// once we can use ES7 property initializers. -// https://github.com/babel/babel/issues/619 - -StaticLocation.prototype.push = throwCannotModify; -StaticLocation.prototype.replace = throwCannotModify; -StaticLocation.prototype.pop = throwCannotModify; - -module.exports = StaticLocation; diff --git a/modules/locations/TestLocation.js b/modules/locations/TestLocation.js deleted file mode 100644 index 76cab8ad65..0000000000 --- a/modules/locations/TestLocation.js +++ /dev/null @@ -1,77 +0,0 @@ -var invariant = require('react/lib/invariant'); -var LocationActions = require('../actions/LocationActions'); -var History = require('../History'); - -/** - * A location that is convenient for testing and does not require a DOM. - */ -class TestLocation { - - constructor(history) { - this.history = history || []; - this.listeners = []; - this._updateHistoryLength(); - } - - get needsDOM() { - return false; - } - - _updateHistoryLength() { - History.length = this.history.length; - } - - _notifyChange(type) { - var change = { - path: this.getCurrentPath(), - type: type - }; - - for (var i = 0, len = this.listeners.length; i < len; ++i) - this.listeners[i].call(this, change); - } - - addChangeListener(listener) { - this.listeners.push(listener); - } - - removeChangeListener(listener) { - this.listeners = this.listeners.filter(function (l) { - return l !== listener; - }); - } - - push(path) { - this.history.push(path); - this._updateHistoryLength(); - this._notifyChange(LocationActions.PUSH); - } - - replace(path) { - invariant( - this.history.length, - 'You cannot replace the current path with no history' - ); - - this.history[this.history.length - 1] = path; - - this._notifyChange(LocationActions.REPLACE); - } - - pop() { - this.history.pop(); - this._updateHistoryLength(); - this._notifyChange(LocationActions.POP); - } - - getCurrentPath() { - return this.history[this.history.length - 1]; - } - - toString() { - return ''; - } - -} - -module.exports = TestLocation; diff --git a/modules/locations/__tests__/HashLocation-test.js b/modules/locations/__tests__/HashLocation-test.js deleted file mode 100644 index 574f1c96b5..0000000000 --- a/modules/locations/__tests__/HashLocation-test.js +++ /dev/null @@ -1,45 +0,0 @@ -var expect = require('expect'); -var HashLocation = require('../HashLocation'); - -describe('HashLocation.getCurrentPath', function () { - afterEach(function () { - window.location.hash = ''; - }); - - //this test is needed because Firefox will pre-decode the value retrieved from - //window.location.hash - it('returns a properly decoded equivalent of window.location.hash', function () { - window.location.hash = ''; - expect(HashLocation.getCurrentPath()).toBe(''); - - window.location.hash = 'asdf'; - expect(HashLocation.getCurrentPath()).toBe('asdf'); - - // + is only special in the query component, not the hash - window.location.hash = 'test+spaces'; - expect(HashLocation.getCurrentPath()).toBe('test+spaces'); - - window.location.hash = 'first%2Fsecond'; - expect(HashLocation.getCurrentPath()).toBe('first%2Fsecond'); - - window.location.hash = 'first/second'; - expect(HashLocation.getCurrentPath()).toBe('first/second'); - - window.location.hash = 'first%252Fsecond'; - expect(HashLocation.getCurrentPath()).toBe('first%2Fsecond'); - - // decodeURI doesn't handle lone percents - window.location.hash = '%'; - expect(function () { - HashLocation.getCurrentPath(); - }).toThrow(URIError); - - window.location.hash = '%25'; - expect(HashLocation.getCurrentPath()).toBe('%'); - - window.location.hash = - 'complicated+string/full%2Fof%3Fspecial%25chars%2520and%23escapes%E1%88%B4'; - expect(HashLocation.getCurrentPath()) - .toBe('complicated+string/full%2Fof%3Fspecial%chars%20and%23escapesሴ'); - }); -}); diff --git a/tests.webpack.js b/tests.webpack.js index 6459e7ff96..7e4c087c54 100644 --- a/tests.webpack.js +++ b/tests.webpack.js @@ -1,2 +1,3 @@ -var context = require.context('./modules', true, /-test\.js$/); +// var context = require.context('./modules', true, /-test\.js$/); +var context = require.context('./modules/history', true, /-test\.js$/); context.keys().forEach(context);