From 4b8a81681d1e6f06190623e4e5865f3c1b1bd412 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 4 Nov 2019 12:42:47 -0800 Subject: [PATCH 001/133] Remove deprecated files from v4 --- DOMUtils.js | 3 --- ExecutionEnvironment.js | 3 --- LocationUtils.js | 3 --- PathUtils.js | 3 --- createBrowserHistory.js | 3 --- createHashHistory.js | 3 --- createMemoryHistory.js | 3 --- createTransitionManager.js | 3 --- es/DOMUtils.js | 7 ------ es/ExecutionEnvironment.js | 7 ------ es/LocationUtils.js | 7 ------ es/PathUtils.js | 7 ------ es/createBrowserHistory.js | 7 ------ es/createHashHistory.js | 7 ------ es/createMemoryHistory.js | 7 ------ es/createTransitionManager.js | 7 ------ es/warnAboutDeprecatedESMImport.js | 35 ------------------------------ package.json | 12 +--------- warnAboutDeprecatedCJSRequire.js | 35 ------------------------------ 19 files changed, 1 insertion(+), 161 deletions(-) delete mode 100644 DOMUtils.js delete mode 100644 ExecutionEnvironment.js delete mode 100644 LocationUtils.js delete mode 100644 PathUtils.js delete mode 100644 createBrowserHistory.js delete mode 100644 createHashHistory.js delete mode 100644 createMemoryHistory.js delete mode 100644 createTransitionManager.js delete mode 100644 es/DOMUtils.js delete mode 100644 es/ExecutionEnvironment.js delete mode 100644 es/LocationUtils.js delete mode 100644 es/PathUtils.js delete mode 100644 es/createBrowserHistory.js delete mode 100644 es/createHashHistory.js delete mode 100644 es/createMemoryHistory.js delete mode 100644 es/createTransitionManager.js delete mode 100644 es/warnAboutDeprecatedESMImport.js delete mode 100644 warnAboutDeprecatedCJSRequire.js diff --git a/DOMUtils.js b/DOMUtils.js deleted file mode 100644 index c4f93d31a..000000000 --- a/DOMUtils.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; -require('./warnAboutDeprecatedCJSRequire.js')('DOMUtils'); -module.exports = require('./index.js').DOMUtils; diff --git a/ExecutionEnvironment.js b/ExecutionEnvironment.js deleted file mode 100644 index 44de3b2f0..000000000 --- a/ExecutionEnvironment.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; -require('./warnAboutDeprecatedCJSRequire.js')('ExecutionEnvironment'); -module.exports = require('./index.js').ExecutionEnvironment; diff --git a/LocationUtils.js b/LocationUtils.js deleted file mode 100644 index a86120560..000000000 --- a/LocationUtils.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; -require('./warnAboutDeprecatedCJSRequire.js')('LocationUtils'); -module.exports = require('./index.js').LocationUtils; diff --git a/PathUtils.js b/PathUtils.js deleted file mode 100644 index 7dc98c205..000000000 --- a/PathUtils.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; -require('./warnAboutDeprecatedCJSRequire.js')('PathUtils'); -module.exports = require('./index.js').PathUtils; diff --git a/createBrowserHistory.js b/createBrowserHistory.js deleted file mode 100644 index 34f12c399..000000000 --- a/createBrowserHistory.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; -require('./warnAboutDeprecatedCJSRequire.js')('createBrowserHistory'); -module.exports = require('./index.js').createBrowserHistory; diff --git a/createHashHistory.js b/createHashHistory.js deleted file mode 100644 index 7686422de..000000000 --- a/createHashHistory.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; -require('./warnAboutDeprecatedCJSRequire.js')('createHashHistory'); -module.exports = require('./index.js').createHashHistory; diff --git a/createMemoryHistory.js b/createMemoryHistory.js deleted file mode 100644 index a0ce3ff40..000000000 --- a/createMemoryHistory.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; -require('./warnAboutDeprecatedCJSRequire.js')('createMemoryHistory'); -module.exports = require('./index.js').createMemoryHistory; diff --git a/createTransitionManager.js b/createTransitionManager.js deleted file mode 100644 index f8cf2f6b7..000000000 --- a/createTransitionManager.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; -require('./warnAboutDeprecatedCJSRequire.js')('createTransitionManager'); -module.exports = require('./index.js').createTransitionManager; diff --git a/es/DOMUtils.js b/es/DOMUtils.js deleted file mode 100644 index 265a991c4..000000000 --- a/es/DOMUtils.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -import warnAboutDeprecatedESMImport from './warnAboutDeprecatedESMImport.js'; -warnAboutDeprecatedESMImport('DOMUtils'); - -import { DOMUtils } from '../esm/history.js'; -export default DOMUtils; diff --git a/es/ExecutionEnvironment.js b/es/ExecutionEnvironment.js deleted file mode 100644 index 9043609bc..000000000 --- a/es/ExecutionEnvironment.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -import warnAboutDeprecatedESMImport from './warnAboutDeprecatedESMImport.js'; -warnAboutDeprecatedESMImport('ExecutionEnvironment'); - -import { ExecutionEnvironment } from '../esm/history.js'; -export default ExecutionEnvironment; diff --git a/es/LocationUtils.js b/es/LocationUtils.js deleted file mode 100644 index 3dc1816b6..000000000 --- a/es/LocationUtils.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -import warnAboutDeprecatedESMImport from './warnAboutDeprecatedESMImport.js'; -warnAboutDeprecatedESMImport('LocationUtils'); - -import { LocationUtils } from '../esm/history.js'; -export default LocationUtils; diff --git a/es/PathUtils.js b/es/PathUtils.js deleted file mode 100644 index 7414d2662..000000000 --- a/es/PathUtils.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -import warnAboutDeprecatedESMImport from './warnAboutDeprecatedESMImport.js'; -warnAboutDeprecatedESMImport('PathUtils'); - -import { PathUtils } from '../esm/history.js'; -export default PathUtils; diff --git a/es/createBrowserHistory.js b/es/createBrowserHistory.js deleted file mode 100644 index c05b10528..000000000 --- a/es/createBrowserHistory.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -import warnAboutDeprecatedESMImport from './warnAboutDeprecatedESMImport.js'; -warnAboutDeprecatedESMImport('createBrowserHistory'); - -import { createBrowserHistory } from '../esm/history.js'; -export default createBrowserHistory; diff --git a/es/createHashHistory.js b/es/createHashHistory.js deleted file mode 100644 index ff40c25b8..000000000 --- a/es/createHashHistory.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -import warnAboutDeprecatedESMImport from './warnAboutDeprecatedESMImport.js'; -warnAboutDeprecatedESMImport('createHashHistory'); - -import { createHashHistory } from '../esm/history.js'; -export default createHashHistory; diff --git a/es/createMemoryHistory.js b/es/createMemoryHistory.js deleted file mode 100644 index 1cc4caab4..000000000 --- a/es/createMemoryHistory.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -import warnAboutDeprecatedESMImport from './warnAboutDeprecatedESMImport.js'; -warnAboutDeprecatedESMImport('createMemoryHistory'); - -import { createMemoryHistory } from '../esm/history.js'; -export default createMemoryHistory; diff --git a/es/createTransitionManager.js b/es/createTransitionManager.js deleted file mode 100644 index 37d454068..000000000 --- a/es/createTransitionManager.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -import warnAboutDeprecatedESMImport from './warnAboutDeprecatedESMImport.js'; -warnAboutDeprecatedESMImport('createTransitionManager'); - -import { createTransitionManager } from '../esm/history.js'; -export default createTransitionManager; diff --git a/es/warnAboutDeprecatedESMImport.js b/es/warnAboutDeprecatedESMImport.js deleted file mode 100644 index 8215fb286..000000000 --- a/es/warnAboutDeprecatedESMImport.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict'; - -var printWarning = function() {}; - -if (process.env.NODE_ENV !== 'production') { - printWarning = function(format, subs) { - var index = 0; - var message = - 'Warning: ' + - (subs.length > 0 - ? format.replace(/%s/g, function() { - return subs[index++]; - }) - : format); - - if (typeof console !== 'undefined') { - console.error(message); - } - - try { - // --- Welcome to debugging history --- - // This error was thrown as a convenience so that you can use the - // stack trace to find the callsite that triggered this warning. - throw new Error(message); - } catch (e) {} - }; -} - -export default function(member) { - printWarning( - 'Please use `import { %s } from "history"` instead of `import %s from "history/es/%s"`. ' + - 'Support for the latter will be removed in the next major release.', - [member, member] - ); -} diff --git a/package.json b/package.json index e48a21a7e..442d7f1a7 100644 --- a/package.json +++ b/package.json @@ -9,19 +9,9 @@ "module": "esm/history.js", "unpkg": "umd/history.js", "files": [ - "DOMUtils.js", - "ExecutionEnvironment.js", - "LocationUtils.js", - "PathUtils.js", "cjs", - "createBrowserHistory.js", - "createHashHistory.js", - "createMemoryHistory.js", - "createTransitionManager.js", - "es", "esm", - "umd", - "warnAboutDeprecatedCJSRequire.js" + "umd" ], "sideEffects": false, "scripts": { diff --git a/warnAboutDeprecatedCJSRequire.js b/warnAboutDeprecatedCJSRequire.js deleted file mode 100644 index 39578596b..000000000 --- a/warnAboutDeprecatedCJSRequire.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict'; - -var printWarning = function() {}; - -if (process.env.NODE_ENV !== 'production') { - printWarning = function(format, subs) { - var index = 0; - var message = - 'Warning: ' + - (subs.length > 0 - ? format.replace(/%s/g, function() { - return subs[index++]; - }) - : format); - - if (typeof console !== 'undefined') { - console.error(message); - } - - try { - // --- Welcome to debugging history --- - // This error was thrown as a convenience so that you can use the - // stack trace to find the callsite that triggered this warning. - throw new Error(message); - } catch (e) {} - }; -} - -module.exports = function(member) { - printWarning( - 'Please use `require("history").%s` instead of `require("history/%s")`. ' + - 'Support for the latter will be removed in the next major release.', - [member, member] - ); -}; From c013f2f7b939a4470480cba2cd99b316283b259d Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 4 Nov 2019 12:44:37 -0800 Subject: [PATCH 002/133] Update ESLint config --- .eslintignore | 3 +++ .eslintrc | 23 +++++++++++++++++++++++ modules/.eslintrc | 14 ++++---------- package.json | 2 +- 4 files changed, 31 insertions(+), 11 deletions(-) create mode 100644 .eslintignore create mode 100644 .eslintrc diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..a632b8fc8 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +/cjs +/esm +/umd diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000..79afbd3b3 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,23 @@ +{ + "parser": "babel-eslint", + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + }, + "plugins": ["import"], + "env": { + "node": true + }, + "extends": ["eslint:recommended", "plugin:import/errors"], + "rules": { + "prefer-arrow-callback": "error", + "no-unused-vars": [ + "warn", + { + "args": "after-used", + "ignoreRestSiblings": true, + "argsIgnorePattern": "event" + } + ] + } +} diff --git a/modules/.eslintrc b/modules/.eslintrc index dc32b8404..340546cde 100644 --- a/modules/.eslintrc +++ b/modules/.eslintrc @@ -1,15 +1,9 @@ { - "parser": "babel-eslint", - "plugins": ["import"], "env": { - "browser": true + "browser": true, + "node": false }, - "extends": ["eslint:recommended", "plugin:import/errors"], - "rules": { - "prefer-arrow-callback": 2 - }, - "parserOptions": { - "ecmaVersion": 6, - "sourceType": "module" + "globals": { + "__DEV__": true } } diff --git a/package.json b/package.json index 442d7f1a7..4d2df9107 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "scripts": { "build": "rollup -c", "clean": "git clean -fdX .", - "lint": "eslint modules", + "lint": "eslint .", "prepublishOnly": "yarn build", "test": "karma start --single-run" }, From 5416474abea4d36ba00066fad5b35be0f1bde11d Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 4 Nov 2019 23:15:43 -0800 Subject: [PATCH 003/133] Main v5 rewrite New features: - Remove legacy browser support (pre pushState) - Add state to hash history - Use custom window when creating history objects - Better history.block API (wip) - Fix location.pathname encoding issues - About 50% smaller - No dependencies Removed features: - Removes basename support - Removes getUserConfirmation - Removes keyLength - Removes hashType - Removes relative pathname support in hash + memory histories Still TBD: - Missing pathname support in push/replace Fixes #624 Fixes #704 Fixes #723 Fixes #726 --- .size-snapshot.json | 24 +- modules/DOMUtils.js | 54 --- modules/LocationUtils.js | 80 ---- modules/PathUtils.js | 59 --- .../__tests__/BrowserHistory-basename-test.js | 88 ----- modules/__tests__/BrowserHistory-test.js | 214 ---------- .../__tests__/HashHistory-basename-test.js | 92 ----- modules/__tests__/HashHistory-coding-test.js | 76 ---- modules/__tests__/HashHistory-test.js | 215 ---------- modules/__tests__/MemoryHistory-test.js | 203 ---------- .../TestSequences/BackButtonTransitionHook.js | 13 +- .../TestSequences/BlockEverything.js | 6 +- .../TestSequences/BlockPopWithoutListening.js | 4 +- modules/__tests__/TestSequences/DenyGoBack.js | 42 -- .../__tests__/TestSequences/DenyGoForward.js | 50 --- modules/__tests__/TestSequences/DenyPush.js | 31 -- .../EncodedReservedCharacters.js | 14 +- modules/__tests__/TestSequences/GoBack.js | 10 +- modules/__tests__/TestSequences/GoForward.js | 14 +- .../TestSequences/HashChangeTransitionHook.js | 33 -- .../TestSequences/HashbangHashPathCoding.js | 48 --- ...nNoKey.js => InitialLocationDefaultKey.js} | 6 +- .../TestSequences/InitialLocationHasKey.js | 4 +- modules/__tests__/TestSequences/Listen.js | 4 +- .../LocationPathnameAlwaysSame.js | 26 +- .../TestSequences/NoslashHashPathCoding.js | 50 --- .../TestSequences/PushEncodedLocation.js | 15 +- .../TestSequences/PushInvalidPathname.js | 17 - .../TestSequences/PushMissingPathname.js | 8 +- .../TestSequences/PushNewLocation.js | 6 +- .../TestSequences/PushRelativePathname.js | 8 +- .../PushRelativePathnameError.js | 31 ++ .../__tests__/TestSequences/PushSamePath.js | 12 +- .../TestSequences/PushSamePathWarning.js | 55 --- modules/__tests__/TestSequences/PushState.js | 6 +- .../TestSequences/PushStateWarning.js | 40 -- .../TestSequences/PushUnicodeLocation.js | 21 +- .../TestSequences/ReplaceInvalidPathname.js | 17 - .../TestSequences/ReplaceNewLocation.js | 6 +- .../TestSequences/ReplaceSamePath.js | 8 +- .../__tests__/TestSequences/ReplaceState.js | 6 +- .../TestSequences/ReplaceStateWarning.js | 40 -- .../ReturnFalseTransitionHook.js | 32 -- .../TestSequences/SlashHashPathCoding.js | 48 --- .../TestSequences/TransitionHookArgs.js | 32 -- modules/__tests__/TestSequences/execSteps.js | 12 +- modules/__tests__/TestSequences/index.js | 46 --- .../__tests__/createBrowserHistory-test.js | 163 ++++++++ ...test.js => createHashHistory-base-test.js} | 4 + modules/__tests__/createHashHistory-test.js | 167 ++++++++ modules/__tests__/createLocation-test.js | 144 ------- modules/__tests__/createMemoryHistory-test.js | 139 +++++++ modules/createBrowserHistory.js | 329 ---------------- modules/createHashHistory.js | 361 ----------------- modules/createMemoryHistory.js | 186 --------- modules/createTransitionManager.js | 78 ---- modules/index.js | 368 +++++++++++++++++- modules/invariant.js | 1 - modules/warning.js | 1 - package.json | 6 +- yarn.lock | 20 - 61 files changed, 989 insertions(+), 2904 deletions(-) delete mode 100644 modules/DOMUtils.js delete mode 100644 modules/LocationUtils.js delete mode 100644 modules/PathUtils.js delete mode 100644 modules/__tests__/BrowserHistory-basename-test.js delete mode 100644 modules/__tests__/BrowserHistory-test.js delete mode 100644 modules/__tests__/HashHistory-basename-test.js delete mode 100644 modules/__tests__/HashHistory-coding-test.js delete mode 100644 modules/__tests__/HashHistory-test.js delete mode 100644 modules/__tests__/MemoryHistory-test.js delete mode 100644 modules/__tests__/TestSequences/DenyGoBack.js delete mode 100644 modules/__tests__/TestSequences/DenyGoForward.js delete mode 100644 modules/__tests__/TestSequences/DenyPush.js delete mode 100644 modules/__tests__/TestSequences/HashChangeTransitionHook.js delete mode 100644 modules/__tests__/TestSequences/HashbangHashPathCoding.js rename modules/__tests__/TestSequences/{InitialLocationNoKey.js => InitialLocationDefaultKey.js} (65%) delete mode 100644 modules/__tests__/TestSequences/NoslashHashPathCoding.js delete mode 100644 modules/__tests__/TestSequences/PushInvalidPathname.js create mode 100644 modules/__tests__/TestSequences/PushRelativePathnameError.js delete mode 100644 modules/__tests__/TestSequences/PushSamePathWarning.js delete mode 100644 modules/__tests__/TestSequences/PushStateWarning.js delete mode 100644 modules/__tests__/TestSequences/ReplaceInvalidPathname.js delete mode 100644 modules/__tests__/TestSequences/ReplaceStateWarning.js delete mode 100644 modules/__tests__/TestSequences/ReturnFalseTransitionHook.js delete mode 100644 modules/__tests__/TestSequences/SlashHashPathCoding.js delete mode 100644 modules/__tests__/TestSequences/TransitionHookArgs.js delete mode 100644 modules/__tests__/TestSequences/index.js create mode 100644 modules/__tests__/createBrowserHistory-test.js rename modules/__tests__/{HashHistory-base-test.js => createHashHistory-base-test.js} (91%) create mode 100644 modules/__tests__/createHashHistory-test.js delete mode 100644 modules/__tests__/createLocation-test.js create mode 100644 modules/__tests__/createMemoryHistory-test.js delete mode 100644 modules/createBrowserHistory.js delete mode 100644 modules/createHashHistory.js delete mode 100644 modules/createMemoryHistory.js delete mode 100644 modules/createTransitionManager.js delete mode 100644 modules/invariant.js delete mode 100644 modules/warning.js diff --git a/.size-snapshot.json b/.size-snapshot.json index c9b0bf5e6..e02c3164f 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -1,26 +1,26 @@ { "esm/history.js": { - "bundled": 28076, - "minified": 12353, - "gzipped": 3575, + "bundled": 13433, + "minified": 5829, + "gzipped": 1706, "treeshaked": { "rollup": { - "code": 208, - "import_statements": 132 + "code": 43, + "import_statements": 43 }, "webpack": { - "code": 1324 + "code": 1027 } } }, "umd/history.js": { - "bundled": 33021, - "minified": 11943, - "gzipped": 3917 + "bundled": 14957, + "minified": 5450, + "gzipped": 1760 }, "umd/history.min.js": { - "bundled": 30384, - "minified": 9993, - "gzipped": 3501 + "bundled": 14343, + "minified": 5137, + "gzipped": 1619 } } diff --git a/modules/DOMUtils.js b/modules/DOMUtils.js deleted file mode 100644 index 95c3a6ea6..000000000 --- a/modules/DOMUtils.js +++ /dev/null @@ -1,54 +0,0 @@ -export const canUseDOM = !!( - typeof window !== 'undefined' && - window.document && - window.document.createElement -); - -export function getConfirmation(message, callback) { - callback(window.confirm(message)); // eslint-disable-line no-alert -} - -/** - * Returns true if the HTML5 history API is supported. Taken from Modernizr. - * - * https://github.com/Modernizr/Modernizr/blob/master/LICENSE - * https://github.com/Modernizr/Modernizr/blob/master/feature-detects/history.js - * changed to avoid false negatives for Windows Phones: https://github.com/reactjs/react-router/issues/586 - */ -export function supportsHistory() { - const ua = window.navigator.userAgent; - - if ( - (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) && - ua.indexOf('Mobile Safari') !== -1 && - ua.indexOf('Chrome') === -1 && - ua.indexOf('Windows Phone') === -1 - ) - return false; - - return window.history && 'pushState' in window.history; -} - -/** - * Returns true if browser fires popstate on hash change. - * IE10 and IE11 do not. - */ -export function supportsPopStateOnHashChange() { - return window.navigator.userAgent.indexOf('Trident') === -1; -} - -/** - * Returns false if using go(n) with hash history causes a full page reload. - */ -export function supportsGoWithoutReloadUsingHash() { - return window.navigator.userAgent.indexOf('Firefox') === -1; -} - -/** - * Returns true if a given popstate event is an extraneous WebKit event. - * Accounts for the fact that Chrome on iOS fires real popstate events - * containing undefined state when pressing the back button. - */ -export function isExtraneousPopstateEvent(event) { - return event.state === undefined && navigator.userAgent.indexOf('CriOS') === -1; -} diff --git a/modules/LocationUtils.js b/modules/LocationUtils.js deleted file mode 100644 index 27cb0f88d..000000000 --- a/modules/LocationUtils.js +++ /dev/null @@ -1,80 +0,0 @@ -import resolvePathname from 'resolve-pathname'; -import valueEqual from 'value-equal'; - -import { parsePath } from './PathUtils.js'; - -export function createLocation(path, state, key, currentLocation) { - let location; - if (typeof path === 'string') { - // Two-arg form: push(path, state) - location = parsePath(path); - location.state = state; - } else { - // One-arg form: push(location) - location = { ...path }; - - if (location.pathname === undefined) location.pathname = ''; - - if (location.search) { - if (location.search.charAt(0) !== '?') - location.search = '?' + location.search; - } else { - location.search = ''; - } - - if (location.hash) { - if (location.hash.charAt(0) !== '#') location.hash = '#' + location.hash; - } else { - location.hash = ''; - } - - if (state !== undefined && location.state === undefined) - location.state = state; - } - - try { - location.pathname = decodeURI(location.pathname); - } catch (e) { - if (e instanceof URIError) { - throw new URIError( - 'Pathname "' + - location.pathname + - '" could not be decoded. ' + - 'This is likely caused by an invalid percent-encoding.' - ); - } else { - throw e; - } - } - - if (key) location.key = key; - - if (currentLocation) { - // Resolve incomplete/relative pathname relative to current location. - if (!location.pathname) { - location.pathname = currentLocation.pathname; - } else if (location.pathname.charAt(0) !== '/') { - location.pathname = resolvePathname( - location.pathname, - currentLocation.pathname - ); - } - } else { - // When there is no prior location and pathname is empty, set it to / - if (!location.pathname) { - location.pathname = '/'; - } - } - - return location; -} - -export function locationsAreEqual(a, b) { - return ( - a.pathname === b.pathname && - a.search === b.search && - a.hash === b.hash && - a.key === b.key && - valueEqual(a.state, b.state) - ); -} diff --git a/modules/PathUtils.js b/modules/PathUtils.js deleted file mode 100644 index e5abec7d2..000000000 --- a/modules/PathUtils.js +++ /dev/null @@ -1,59 +0,0 @@ -export function addLeadingSlash(path) { - return path.charAt(0) === '/' ? path : '/' + path; -} - -export function stripLeadingSlash(path) { - return path.charAt(0) === '/' ? path.substr(1) : path; -} - -export function hasBasename(path, prefix) { - return ( - path.toLowerCase().indexOf(prefix.toLowerCase()) === 0 && - '/?#'.indexOf(path.charAt(prefix.length)) !== -1 - ); -} - -export function stripBasename(path, prefix) { - return hasBasename(path, prefix) ? path.substr(prefix.length) : path; -} - -export function stripTrailingSlash(path) { - return path.charAt(path.length - 1) === '/' ? path.slice(0, -1) : path; -} - -export function parsePath(path) { - let pathname = path || '/'; - let search = ''; - let hash = ''; - - const hashIndex = pathname.indexOf('#'); - if (hashIndex !== -1) { - hash = pathname.substr(hashIndex); - pathname = pathname.substr(0, hashIndex); - } - - const searchIndex = pathname.indexOf('?'); - if (searchIndex !== -1) { - search = pathname.substr(searchIndex); - pathname = pathname.substr(0, searchIndex); - } - - return { - pathname, - search: search === '?' ? '' : search, - hash: hash === '#' ? '' : hash - }; -} - -export function createPath(location) { - const { pathname, search, hash } = location; - - let path = pathname || '/'; - - if (search && search !== '?') - path += search.charAt(0) === '?' ? search : `?${search}`; - - if (hash && hash !== '#') path += hash.charAt(0) === '#' ? hash : `#${hash}`; - - return path; -} diff --git a/modules/__tests__/BrowserHistory-basename-test.js b/modules/__tests__/BrowserHistory-basename-test.js deleted file mode 100644 index 97a67fa90..000000000 --- a/modules/__tests__/BrowserHistory-basename-test.js +++ /dev/null @@ -1,88 +0,0 @@ -import expect from 'expect'; -import mock from 'jest-mock'; -import { createBrowserHistory } from 'history'; - -describe('a browser history with a basename', () => { - it('knows how to create hrefs', () => { - window.history.replaceState(null, null, '/the/base'); - const history = createBrowserHistory({ basename: '/the/base' }); - const href = history.createHref({ - pathname: '/the/path', - search: '?the=query', - hash: '#the-hash' - }); - - expect(href).toEqual('/the/base/the/path?the=query#the-hash'); - }); - - describe('with a bad basename', () => { - it('knows how to create hrefs', () => { - window.history.replaceState(null, null, '/the/bad/base'); - const history = createBrowserHistory({ basename: '/the/bad/base/' }); - const href = history.createHref({ - pathname: '/the/path', - search: '?the=query', - hash: '#the-hash' - }); - - expect(href).toEqual('/the/bad/base/the/path?the=query#the-hash'); - }); - }); - - describe('with a slash basename', () => { - it('knows how to create hrefs', () => { - window.history.replaceState(null, null, '/'); - const history = createBrowserHistory({ basename: '/' }); - const href = history.createHref({ - pathname: '/the/path', - search: '?the=query', - hash: '#the-hash' - }); - - expect(href).toEqual('/the/path?the=query#the-hash'); - }); - }); - - it('strips the basename from the pathname', () => { - window.history.replaceState(null, null, '/prefix/pathname'); - const history = createBrowserHistory({ basename: '/prefix' }); - expect(history.location.pathname).toEqual('/pathname'); - }); - - it('is not case-sensitive', () => { - window.history.replaceState(null, null, '/PREFIX/pathname'); - const history = createBrowserHistory({ basename: '/prefix' }); - expect(history.location.pathname).toEqual('/pathname'); - }); - - it('does not strip partial prefix matches', () => { - const spy = mock.spyOn(console, 'warn').mockImplementation(() => {}); - - window.history.replaceState(null, null, '/prefixed/pathname'); - const history = createBrowserHistory({ basename: '/prefix' }); - expect(history.location.pathname).toEqual('/prefixed/pathname'); - - expect(spy).toHaveBeenCalledTimes(1); - spy.mockRestore(); - }); - - describe('when the pathname is only the prefix', () => { - it('strips the prefix', () => { - window.history.replaceState(null, null, '/prefix'); - const history = createBrowserHistory({ basename: '/prefix' }); - expect(history.location.pathname).toEqual('/'); - }); - - it('strips the prefix when there is a search string', () => { - window.history.replaceState(null, null, '/prefix?a=b'); - const history = createBrowserHistory({ basename: '/prefix' }); - expect(history.location.pathname).toEqual('/'); - }); - - it('strips the prefix when there is a hash', () => { - window.history.replaceState(null, null, '/prefix#rest'); - const history = createBrowserHistory({ basename: '/prefix' }); - expect(history.location.pathname).toEqual('/'); - }); - }); -}); diff --git a/modules/__tests__/BrowserHistory-test.js b/modules/__tests__/BrowserHistory-test.js deleted file mode 100644 index a9ac32d66..000000000 --- a/modules/__tests__/BrowserHistory-test.js +++ /dev/null @@ -1,214 +0,0 @@ -import expect from 'expect'; -import { createBrowserHistory } from 'history'; - -import * as TestSequences from './TestSequences/index.js'; - -describe('a browser history', () => { - let history; - beforeEach(() => { - if (window.location.pathname !== '/') { - window.history.replaceState(null, null, '/'); - } - history = createBrowserHistory(); - }); - - it('knows how to create hrefs', () => { - const href = history.createHref({ - pathname: '/the/path', - search: '?the=query', - hash: '#the-hash' - }); - - expect(href).toEqual('/the/path?the=query#the-hash'); - }); - - it('does not encode the generated path', () => { - // encoded - const encodedHref = history.createHref({ - pathname: '/%23abc' - }); - // unencoded - const unencodedHref = history.createHref({ - pathname: '/#abc' - }); - - expect(encodedHref).toEqual('/%23abc'); - expect(unencodedHref).toEqual('/#abc'); - }); - - describe('listen', () => { - it('does not immediately call listeners', done => { - TestSequences.Listen(history, done); - }); - }); - - describe('the initial location', () => { - it('does not have a key', done => { - TestSequences.InitialLocationNoKey(history, done); - }); - }); - - describe('push a new path', () => { - it('calls change listeners with the new location', done => { - TestSequences.PushNewLocation(history, done); - }); - }); - - describe('push the same path', () => { - it('calls change listeners with the new location', done => { - TestSequences.PushSamePath(history, done); - }); - }); - - describe('push state', () => { - it('calls change listeners with the new location', done => { - TestSequences.PushState(history, done); - }); - }); - - describe('push with no pathname', () => { - it('calls change listeners with the normalized location', done => { - TestSequences.PushMissingPathname(history, done); - }); - }); - - describe('push with a relative pathname', () => { - it('calls change listeners with the normalized location', done => { - TestSequences.PushRelativePathname(history, done); - }); - }); - - describe('push with an invalid pathname (bad percent-encoding)', () => { - it('throws an error', done => { - TestSequences.PushInvalidPathname(history, done); - }); - }); - - describe('push with a unicode path string', () => { - it('creates a location with decoded properties', done => { - TestSequences.PushUnicodeLocation(history, done); - }); - }); - - describe('push with an encoded path string', () => { - it('creates a location object with encoded pathname', done => { - TestSequences.PushEncodedLocation(history, done); - }); - }); - - describe('replace a new path', () => { - it('calls change listeners with the new location', done => { - TestSequences.ReplaceNewLocation(history, done); - }); - }); - - describe('replace the same path', () => { - it('calls change listeners with the new location', done => { - TestSequences.ReplaceSamePath(history, done); - }); - }); - - describe('replace with an invalid pathname (bad percent-encoding)', () => { - it('throws an error', done => { - TestSequences.ReplaceInvalidPathname(history, done); - }); - }); - - describe('replace state', () => { - it('calls change listeners with the new location', done => { - TestSequences.ReplaceState(history, done); - }); - }); - - describe('location created by encoded and unencoded pathname', () => { - it('produces the same location.pathname', done => { - TestSequences.LocationPathnameAlwaysSame(history, done); - }); - }); - - describe('location created with encoded/unencoded reserved characters', () => { - it('produces different location objects', done => { - TestSequences.EncodedReservedCharacters(history, done); - }); - }); - - describe('goBack', () => { - it('calls change listeners with the previous location', done => { - TestSequences.GoBack(history, done); - }); - }); - - describe('goForward', () => { - it('calls change listeners with the next location', done => { - TestSequences.GoForward(history, done); - }); - }); - - describe('block', () => { - it('blocks all transitions', done => { - TestSequences.BlockEverything(history, done); - }); - }); - - describe('block a POP without listening', () => { - it('receives the next location and action as arguments', done => { - TestSequences.BlockPopWithoutListening(history, done); - }); - }); - - describe('that accepts all transitions', () => { - let history; - beforeEach(() => { - history = createBrowserHistory({ - getUserConfirmation(_, callback) { - callback(true); - } - }); - }); - - it('receives the next location and action as arguments', done => { - TestSequences.TransitionHookArgs(history, done); - }); - - it('cancels the transition when it returns false', done => { - TestSequences.ReturnFalseTransitionHook(history, done); - }); - - it('is called when the back button is clicked', done => { - TestSequences.BackButtonTransitionHook(history, done); - }); - - it('is called on the hashchange event', done => { - TestSequences.HashChangeTransitionHook(history, done); - }); - }); - - describe('that denies all transitions', () => { - let history; - beforeEach(() => { - history = createBrowserHistory({ - getUserConfirmation(_, callback) { - callback(false); - } - }); - }); - - describe('clicking on a link (push)', () => { - it('does not update the location', done => { - TestSequences.DenyPush(history, done); - }); - }); - - describe('clicking the back button (goBack)', () => { - it('does not update the location', done => { - TestSequences.DenyGoBack(history, done); - }); - }); - - describe('clicking the forward button (goForward)', () => { - it('does not update the location', done => { - TestSequences.DenyGoForward(history, done); - }); - }); - }); -}); diff --git a/modules/__tests__/HashHistory-basename-test.js b/modules/__tests__/HashHistory-basename-test.js deleted file mode 100644 index 1f5e5306b..000000000 --- a/modules/__tests__/HashHistory-basename-test.js +++ /dev/null @@ -1,92 +0,0 @@ -import expect from 'expect'; -import mock from 'jest-mock'; -import { createHashHistory } from 'history'; - -describe('a hash history with a basename', () => { - it('knows how to create hrefs', () => { - window.location.hash = '#/the/base'; - const history = createHashHistory({ basename: '/the/base' }); - const href = history.createHref({ - pathname: '/the/path', - search: '?the=query' - }); - - expect(href).toEqual('#/the/base/the/path?the=query'); - }); - - describe('with a bad basename', () => { - it('knows how to create hrefs', () => { - window.location.hash = '#/the/bad/base/'; - const history = createHashHistory({ basename: '/the/bad/base/' }); - const href = history.createHref({ - pathname: '/the/path', - search: '?the=query' - }); - - expect(href).toEqual('#/the/bad/base/the/path?the=query'); - }); - }); - - describe('with a slash basename', () => { - it('knows how to create hrefs', () => { - const history = createHashHistory({ basename: '/' }); - const href = history.createHref({ - pathname: '/the/path', - search: '?the=query' - }); - - expect(href).toEqual('#/the/path?the=query'); - }); - }); - - it('strips the basename from the pathname', () => { - window.location.hash = '/prefix/hello'; - const history = createHashHistory({ basename: '/prefix' }); - expect(history.location.pathname).toEqual('/hello'); - }); - - it('is not case-sensitive', () => { - window.location.hash = '/PREFIX/hello'; - const history = createHashHistory({ basename: '/prefix' }); - expect(history.location.pathname).toEqual('/hello'); - }); - - it('allows special regex characters', () => { - window.location.hash = '/prefix$special/hello'; - const history = createHashHistory({ basename: '/prefix$special' }); - expect(history.location.pathname).toEqual('/hello'); - }); - - it('does not strip partial prefix matches', () => { - window.location.hash = '/no-match/hello'; - - // A warning is issued when the prefix is not present. - const spy = mock.spyOn(console, 'warn').mockImplementation(() => {}); - - const history = createHashHistory({ basename: '/prefix' }); - expect(history.location.pathname).toEqual('/no-match/hello'); - - expect(spy).toHaveBeenCalledTimes(1); - spy.mockRestore(); - }); - - describe('when the pathname is only the prefix', () => { - it('strips the prefix', () => { - window.location.hash = '/prefix'; - const history = createHashHistory({ basename: '/prefix' }); - expect(history.location.pathname).toEqual('/'); - }); - - it('strips the prefix when there is a search string', () => { - window.location.hash = '/prefix?a=b'; - const history = createHashHistory({ basename: '/prefix' }); - expect(history.location.pathname).toEqual('/'); - }); - - it('strips the prefix when there is a hash', () => { - window.location.hash = '/prefix#hash'; - const history = createHashHistory({ basename: '/prefix' }); - expect(history.location.pathname).toEqual('/'); - }); - }); -}); diff --git a/modules/__tests__/HashHistory-coding-test.js b/modules/__tests__/HashHistory-coding-test.js deleted file mode 100644 index 8faae2040..000000000 --- a/modules/__tests__/HashHistory-coding-test.js +++ /dev/null @@ -1,76 +0,0 @@ -import expect from 'expect'; -import { createHashHistory } from 'history'; - -import * as TestSequences from './TestSequences/index.js'; - -describe('a hash history with "slash" path coding', () => { - beforeEach(() => { - if (window.location.hash !== '#/') { - window.location.hash = '/'; - } - }); - - it('knows how to create hrefs', () => { - const history = createHashHistory({ hashType: 'slash' }); - const href = history.createHref({ - pathname: '/the/path', - search: '?the=query', - hash: '#the-hash' - }); - - expect(href).toEqual('#/the/path?the=query#the-hash'); - }); - - it('properly encodes and decodes window.location.hash', done => { - const history = createHashHistory({ hashType: 'slash' }); - TestSequences.SlashHashPathCoding(history, done); - }); -}); - -describe('a hash history with "hashbang" path coding', () => { - beforeEach(() => { - if (window.location.hash !== '#!/') { - window.location.hash = '!/'; - } - }); - - it('knows how to create hrefs', () => { - const history = createHashHistory({ hashType: 'hashbang' }); - const href = history.createHref({ - pathname: '/the/path', - search: '?the=query', - hash: '#the-hash' - }); - - expect(href).toEqual('#!/the/path?the=query#the-hash'); - }); - - it('properly encodes and decodes window.location.hash', done => { - const history = createHashHistory({ hashType: 'hashbang' }); - TestSequences.HashbangHashPathCoding(history, done); - }); -}); - -describe('a hash history with "noslash" path coding', () => { - beforeEach(() => { - if (window.location.hash !== '') { - window.location.hash = ''; - } - }); - - it('knows how to create hrefs', () => { - const history = createHashHistory({ hashType: 'noslash' }); - const href = history.createHref({ - pathname: '/the/path', - search: '?the=query', - hash: '#the-hash' - }); - - expect(href).toEqual('#the/path?the=query#the-hash'); - }); - - it('properly encodes and decodes window.location.hash', done => { - const history = createHashHistory({ hashType: 'noslash' }); - TestSequences.NoslashHashPathCoding(history, done); - }); -}); diff --git a/modules/__tests__/HashHistory-test.js b/modules/__tests__/HashHistory-test.js deleted file mode 100644 index 63c4432f0..000000000 --- a/modules/__tests__/HashHistory-test.js +++ /dev/null @@ -1,215 +0,0 @@ -import expect from 'expect'; -import { createHashHistory } from 'history'; - -import * as TestSequences from './TestSequences/index.js'; - -const canGoWithoutReload = window.navigator.userAgent.indexOf('Firefox') === -1; -const describeGo = canGoWithoutReload ? describe : describe.skip; - -describe('a hash history', () => { - let history; - beforeEach(() => { - if (window.location.hash !== '#/') { - window.location.hash = '/'; - } - history = createHashHistory(); - }); - - it('knows how to create hrefs', () => { - const href = history.createHref({ - pathname: '/the/path', - search: '?the=query', - hash: '#the-hash' - }); - - expect(href).toEqual('#/the/path?the=query#the-hash'); - }); - - it('does not encode the generated path', () => { - // encoded - const encodedHref = history.createHref({ - pathname: '/%23abc' - }); - // unencoded - const unencodedHref = history.createHref({ - pathname: '/#abc' - }); - - expect(encodedHref).toEqual('#/%23abc'); - expect(unencodedHref).toEqual('#/#abc'); - }); - - describe('listen', () => { - it('does not immediately call listeners', done => { - TestSequences.Listen(history, done); - }); - }); - - describe('the initial location', () => { - it('does not have a key', done => { - TestSequences.InitialLocationNoKey(history, done); - }); - }); - - describe('push a new path', () => { - it('calls change listeners with the new location', done => { - TestSequences.PushNewLocation(history, done); - }); - }); - - describe('push the same path', () => { - it('calls change listeners with the same location and emits a warning', done => { - TestSequences.PushSamePathWarning(history, done); - }); - }); - - describe('push state', () => { - it('calls change listeners with the new location and emits a warning', done => { - TestSequences.PushStateWarning(history, done); - }); - }); - - describe('push with no pathname', () => { - it('calls change listeners with the normalized location', done => { - TestSequences.PushMissingPathname(history, done); - }); - }); - - describe('push with a relative pathname', () => { - it('calls change listeners with the normalized location', done => { - TestSequences.PushRelativePathname(history, done); - }); - }); - - describe('push with an invalid pathname (bad percent-encoding)', () => { - it('throws an error', done => { - TestSequences.PushInvalidPathname(history, done); - }); - }); - - describe('push with a unicode path string', () => { - it('creates a location with decoded properties', done => { - TestSequences.PushUnicodeLocation(history, done); - }); - }); - - describe('push with an encoded path string', () => { - it('creates a location object with encoded pathname', done => { - TestSequences.PushEncodedLocation(history, done); - }); - }); - - describe('replace a new path', () => { - it('calls change listeners with the new location', done => { - TestSequences.ReplaceNewLocation(history, done); - }); - }); - - describe('replace the same path', () => { - it('calls change listeners with the new location', done => { - TestSequences.ReplaceSamePath(history, done); - }); - }); - - describe('replace with an invalid pathname (bad percent-encoding)', () => { - it('throws an error', done => { - TestSequences.ReplaceInvalidPathname(history, done); - }); - }); - - describe('replace state', () => { - it('calls change listeners with the new location and emits a warning', done => { - TestSequences.ReplaceStateWarning(history, done); - }); - }); - - describe('location created by encoded and unencoded pathname', () => { - it('produces the same location.pathname', done => { - TestSequences.LocationPathnameAlwaysSame(history, done); - }); - }); - - describe('location created with encoded/unencoded reserved characters', () => { - it('produces different location objects', done => { - TestSequences.EncodedReservedCharacters(history, done); - }); - }); - - describeGo('goBack', () => { - it('calls change listeners with the previous location', done => { - TestSequences.GoBack(history, done); - }); - }); - - describeGo('goForward', () => { - it('calls change listeners with the next location', done => { - TestSequences.GoForward(history, done); - }); - }); - - describe('block', () => { - it('blocks all transitions', done => { - TestSequences.BlockEverything(history, done); - }); - }); - - describeGo('block a POP without listening', () => { - it('receives the next location and action as arguments', done => { - TestSequences.BlockPopWithoutListening(history, done); - }); - }); - - describe('that accepts all transitions', () => { - let history; - beforeEach(() => { - history = createHashHistory({ - getUserConfirmation(_, callback) { - callback(true); - } - }); - }); - - it('receives the next location and action as arguments', done => { - TestSequences.TransitionHookArgs(history, done); - }); - - const itBackButton = canGoWithoutReload ? it : it.skip; - - itBackButton('is called when the back button is clicked', done => { - TestSequences.BackButtonTransitionHook(history, done); - }); - - it('cancels the transition when it returns false', done => { - TestSequences.ReturnFalseTransitionHook(history, done); - }); - }); - - describe('that denies all transitions', () => { - let history; - beforeEach(() => { - history = createHashHistory({ - getUserConfirmation(_, callback) { - callback(false); - } - }); - }); - - describe('clicking on a link (push)', () => { - it('does not update the location', done => { - TestSequences.DenyPush(history, done); - }); - }); - - describeGo('clicking the back button (goBack)', () => { - it('does not update the location', done => { - TestSequences.DenyGoBack(history, done); - }); - }); - - describeGo('clicking the forward button (goForward)', () => { - it('does not update the location', done => { - TestSequences.DenyGoForward(history, done); - }); - }); - }); -}); diff --git a/modules/__tests__/MemoryHistory-test.js b/modules/__tests__/MemoryHistory-test.js deleted file mode 100644 index 176f874fb..000000000 --- a/modules/__tests__/MemoryHistory-test.js +++ /dev/null @@ -1,203 +0,0 @@ -import expect from 'expect'; -import { createMemoryHistory } from 'history'; - -import * as TestSequences from './TestSequences/index.js'; - -describe('a memory history', () => { - let history; - beforeEach(() => { - history = createMemoryHistory(); - }); - - it('knows how to create hrefs', () => { - const href = history.createHref({ - pathname: '/the/path', - search: '?the=query', - hash: '#the-hash' - }); - - expect(href).toEqual('/the/path?the=query#the-hash'); - }); - - it('does not encode the generated path', () => { - // encoded - const encodedHref = history.createHref({ - pathname: '/%23abc' - }); - // unencoded - const unencodedHref = history.createHref({ - pathname: '/#abc' - }); - - expect(encodedHref).toEqual('/%23abc'); - expect(unencodedHref).toEqual('/#abc'); - }); - - describe('listen', () => { - it('does not immediately call listeners', done => { - TestSequences.Listen(history, done); - }); - }); - - describe('the initial location', () => { - it('has a key', done => { - TestSequences.InitialLocationHasKey(history, done); - }); - }); - - describe('push a new path', () => { - it('calls change listeners with the new location', done => { - TestSequences.PushNewLocation(history, done); - }); - }); - - describe('push the same path', () => { - it('calls change listeners with the new location', done => { - TestSequences.PushSamePath(history, done); - }); - }); - - describe('push state', () => { - it('calls change listeners with the new location', done => { - TestSequences.PushState(history, done); - }); - }); - - describe('push with no pathname', () => { - it('calls change listeners with the normalized location', done => { - TestSequences.PushMissingPathname(history, done); - }); - }); - - describe('push with a relative pathname', () => { - it('calls change listeners with the normalized location', done => { - TestSequences.PushRelativePathname(history, done); - }); - }); - - describe('push with an invalid pathname (bad percent-encoding)', () => { - it('throws an error', done => { - TestSequences.PushInvalidPathname(history, done); - }); - }); - - describe('push with a unicode path string', () => { - it('creates a location with decoded properties', done => { - TestSequences.PushUnicodeLocation(history, done); - }); - }); - - describe('push with an encoded path string', () => { - it('creates a location object with encoded pathname', done => { - TestSequences.PushEncodedLocation(history, done); - }); - }); - - describe('replace a new path', () => { - it('calls change listeners with the new location', done => { - TestSequences.ReplaceNewLocation(history, done); - }); - }); - - describe('replace the same path', () => { - it('calls change listeners with the new location', done => { - TestSequences.ReplaceSamePath(history, done); - }); - }); - - describe('replace with an invalid pathname (bad percent-encoding)', () => { - it('throws an error', done => { - TestSequences.ReplaceInvalidPathname(history, done); - }); - }); - - describe('replace state', () => { - it('calls change listeners with the new location', done => { - TestSequences.ReplaceState(history, done); - }); - }); - - describe('location created by encoded and unencoded pathname', () => { - it('produces the same location.pathname', done => { - TestSequences.LocationPathnameAlwaysSame(history, done); - }); - }); - - describe('location created with encoded/unencoded reserved characters', () => { - it('produces different location objects', done => { - TestSequences.EncodedReservedCharacters(history, done); - }); - }); - - describe('goBack', () => { - it('calls change listeners with the previous location', done => { - TestSequences.GoBack(history, done); - }); - }); - - describe('goForward', () => { - it('calls change listeners with the next location', done => { - TestSequences.GoForward(history, done); - }); - }); - - describe('block', () => { - it('blocks all transitions', done => { - TestSequences.BlockEverything(history, done); - }); - }); - - describe('block a POP without listening', () => { - it('receives the next location and action as arguments', done => { - TestSequences.BlockPopWithoutListening(history, done); - }); - }); - - describe('that accepts all transitions', () => { - let history; - beforeEach(() => { - history = createMemoryHistory({ - getUserConfirmation(_, callback) { - callback(true); - } - }); - }); - - it('receives the next location and action as arguments', done => { - TestSequences.TransitionHookArgs(history, done); - }); - - it('cancels the transition when it returns false', done => { - TestSequences.ReturnFalseTransitionHook(history, done); - }); - }); - - describe('that denies all transitions', () => { - let history; - beforeEach(() => { - history = createMemoryHistory({ - getUserConfirmation(_, callback) { - callback(false); - } - }); - }); - - describe('push', () => { - it('does not update the location', done => { - TestSequences.DenyPush(history, done); - }); - }); - - describe('goBack', () => { - it('does not update the location', done => { - TestSequences.DenyGoBack(history, done); - }); - }); - - describe('goForward', () => { - it('does not update the location', done => { - TestSequences.DenyGoForward(history, done); - }); - }); - }); -}); diff --git a/modules/__tests__/TestSequences/BackButtonTransitionHook.js b/modules/__tests__/TestSequences/BackButtonTransitionHook.js index 9c6783c08..d0544e21c 100644 --- a/modules/__tests__/TestSequences/BackButtonTransitionHook.js +++ b/modules/__tests__/TestSequences/BackButtonTransitionHook.js @@ -3,17 +3,18 @@ import expect from 'expect'; import execSteps from './execSteps.js'; export default function(history, done) { - let unblock, - hookWasCalled = false; - const steps = [ - location => { + let hookWasCalled = false; + let unblock; + + let steps = [ + ({ location }) => { expect(location).toMatchObject({ pathname: '/' }); history.push('/home'); }, - (location, action) => { + ({ action, location }) => { expect(action).toBe('PUSH'); expect(location).toMatchObject({ pathname: '/home' @@ -25,7 +26,7 @@ export default function(history, done) { window.history.go(-1); }, - (location, action) => { + ({ action, location }) => { expect(action).toBe('POP'); expect(location).toMatchObject({ pathname: '/' diff --git a/modules/__tests__/TestSequences/BlockEverything.js b/modules/__tests__/TestSequences/BlockEverything.js index b5860c312..ab8db6324 100644 --- a/modules/__tests__/TestSequences/BlockEverything.js +++ b/modules/__tests__/TestSequences/BlockEverything.js @@ -3,13 +3,13 @@ import expect from 'expect'; import execSteps from './execSteps.js'; export default function(history, done) { - const steps = [ - location => { + let steps = [ + ({ location }) => { expect(location).toMatchObject({ pathname: '/' }); - const unblock = history.block(); + let unblock = history.block(); history.push('/home'); diff --git a/modules/__tests__/TestSequences/BlockPopWithoutListening.js b/modules/__tests__/TestSequences/BlockPopWithoutListening.js index 883fce364..f98887edc 100644 --- a/modules/__tests__/TestSequences/BlockPopWithoutListening.js +++ b/modules/__tests__/TestSequences/BlockPopWithoutListening.js @@ -8,7 +8,7 @@ export default function(history, done) { history.push('/home'); let transitionHookWasCalled = false; - const unblock = history.block(() => { + let unblock = history.block(() => { transitionHookWasCalled = true; }); @@ -20,7 +20,7 @@ export default function(history, done) { // Allow some time for history to detect the PUSH. setTimeout(() => { - history.goBack(); + history.back(); // Allow some time for history to detect the POP. setTimeout(() => { diff --git a/modules/__tests__/TestSequences/DenyGoBack.js b/modules/__tests__/TestSequences/DenyGoBack.js deleted file mode 100644 index 122d33675..000000000 --- a/modules/__tests__/TestSequences/DenyGoBack.js +++ /dev/null @@ -1,42 +0,0 @@ -import expect from 'expect'; - -import execSteps from './execSteps.js'; - -export default function(history, done) { - let unblock; - const steps = [ - location => { - expect(location).toMatchObject({ - pathname: '/' - }); - - history.push('/home'); - }, - (location, action) => { - expect(action).toBe('PUSH'); - expect(location).toMatchObject({ - pathname: '/home' - }); - - unblock = history.block(nextLocation => { - expect(nextLocation).toMatchObject({ - pathname: '/' - }); - - return 'Are you sure?'; - }); - - history.goBack(); - }, - (location, action) => { - expect(action).toBe('PUSH'); - expect(location).toMatchObject({ - pathname: '/home' - }); - - unblock(); - } - ]; - - execSteps(steps, history, done); -} diff --git a/modules/__tests__/TestSequences/DenyGoForward.js b/modules/__tests__/TestSequences/DenyGoForward.js deleted file mode 100644 index fa27a933d..000000000 --- a/modules/__tests__/TestSequences/DenyGoForward.js +++ /dev/null @@ -1,50 +0,0 @@ -import expect from 'expect'; - -import execSteps from './execSteps.js'; - -export default function(history, done) { - let unblock; - const steps = [ - location => { - expect(location).toMatchObject({ - pathname: '/' - }); - - history.push('/home'); - }, - (location, action) => { - expect(action).toBe('PUSH'); - expect(location).toMatchObject({ - pathname: '/home' - }); - - history.goBack(); - }, - (location, action) => { - expect(action).toBe('POP'); - expect(location).toMatchObject({ - pathname: '/' - }); - - unblock = history.block(nextLocation => { - expect(nextLocation).toMatchObject({ - pathname: '/home' - }); - - return 'Are you sure?'; - }); - - history.goForward(); - }, - (location, action) => { - expect(action).toBe('POP'); - expect(location).toMatchObject({ - pathname: '/' - }); - - unblock(); - } - ]; - - execSteps(steps, history, done); -} diff --git a/modules/__tests__/TestSequences/DenyPush.js b/modules/__tests__/TestSequences/DenyPush.js deleted file mode 100644 index 1fd82aea3..000000000 --- a/modules/__tests__/TestSequences/DenyPush.js +++ /dev/null @@ -1,31 +0,0 @@ -import expect from 'expect'; - -import execSteps from './execSteps.js'; - -export default function(history, done) { - const steps = [ - location => { - expect(location).toMatchObject({ - pathname: '/' - }); - - const unblock = history.block(nextLocation => { - expect(nextLocation).toMatchObject({ - pathname: '/home' - }); - - return 'Are you sure?'; - }); - - history.push('/home'); - - expect(history.location).toMatchObject({ - pathname: '/' - }); - - unblock(); - } - ]; - - execSteps(steps, history, done); -} diff --git a/modules/__tests__/TestSequences/EncodedReservedCharacters.js b/modules/__tests__/TestSequences/EncodedReservedCharacters.js index 355a2ff00..b0addd0df 100644 --- a/modules/__tests__/TestSequences/EncodedReservedCharacters.js +++ b/modules/__tests__/TestSequences/EncodedReservedCharacters.js @@ -3,30 +3,30 @@ import expect from 'expect'; import execSteps from './execSteps.js'; export default function(history, done) { - const steps = [ + let steps = [ () => { // encoded string - const pathname = '/view/%23abc'; + let pathname = '/view/%23abc'; history.replace(pathname); }, - location => { + ({ location }) => { expect(location).toMatchObject({ pathname: '/view/%23abc' }); // encoded object - const pathname = '/view/%23abc'; + let pathname = '/view/%23abc'; history.replace({ pathname }); }, - location => { + ({ location }) => { expect(location).toMatchObject({ pathname: '/view/%23abc' }); // unencoded string - const pathname = '/view/#abc'; + let pathname = '/view/#abc'; history.replace(pathname); }, - location => { + ({ location }) => { expect(location).toMatchObject({ pathname: '/view/', hash: '#abc' diff --git a/modules/__tests__/TestSequences/GoBack.js b/modules/__tests__/TestSequences/GoBack.js index b5a5752d2..2f582e06f 100644 --- a/modules/__tests__/TestSequences/GoBack.js +++ b/modules/__tests__/TestSequences/GoBack.js @@ -3,23 +3,23 @@ import expect from 'expect'; import execSteps from './execSteps.js'; export default function(history, done) { - const steps = [ - location => { + let steps = [ + ({ location }) => { expect(location).toMatchObject({ pathname: '/' }); history.push('/home'); }, - (location, action) => { + ({ action, location }) => { expect(action).toEqual('PUSH'); expect(location).toMatchObject({ pathname: '/home' }); - history.goBack(); + history.back(); }, - (location, action) => { + ({ action, location }) => { expect(action).toEqual('POP'); expect(location).toMatchObject({ pathname: '/' diff --git a/modules/__tests__/TestSequences/GoForward.js b/modules/__tests__/TestSequences/GoForward.js index 06d7f61af..7a5a6c416 100644 --- a/modules/__tests__/TestSequences/GoForward.js +++ b/modules/__tests__/TestSequences/GoForward.js @@ -3,31 +3,31 @@ import expect from 'expect'; import execSteps from './execSteps.js'; export default function(history, done) { - const steps = [ - location => { + let steps = [ + ({ location }) => { expect(location).toMatchObject({ pathname: '/' }); history.push('/home'); }, - (location, action) => { + ({ action, location }) => { expect(action).toEqual('PUSH'); expect(location).toMatchObject({ pathname: '/home' }); - history.goBack(); + history.back(); }, - (location, action) => { + ({ action, location }) => { expect(action).toEqual('POP'); expect(location).toMatchObject({ pathname: '/' }); - history.goForward(); + history.forward(); }, - (location, action) => { + ({ action, location }) => { expect(action).toEqual('POP'); expect(location).toMatchObject({ pathname: '/home' diff --git a/modules/__tests__/TestSequences/HashChangeTransitionHook.js b/modules/__tests__/TestSequences/HashChangeTransitionHook.js deleted file mode 100644 index 73fdeb917..000000000 --- a/modules/__tests__/TestSequences/HashChangeTransitionHook.js +++ /dev/null @@ -1,33 +0,0 @@ -import expect from 'expect'; - -import execSteps from './execSteps.js'; - -export default function(history, done) { - let unblock, - hookWasCalled = false; - const steps = [ - location => { - expect(location).toMatchObject({ - pathname: '/' - }); - - unblock = history.block(() => { - hookWasCalled = true; - }); - - window.location.hash = 'something-new'; - }, - location => { - expect(location).toMatchObject({ - pathname: '/', - hash: '#something-new' - }); - - expect(hookWasCalled).toBe(true); - - unblock(); - } - ]; - - execSteps(steps, history, done); -} diff --git a/modules/__tests__/TestSequences/HashbangHashPathCoding.js b/modules/__tests__/TestSequences/HashbangHashPathCoding.js deleted file mode 100644 index 3380797d0..000000000 --- a/modules/__tests__/TestSequences/HashbangHashPathCoding.js +++ /dev/null @@ -1,48 +0,0 @@ -import expect from 'expect'; - -import execSteps from './execSteps.js'; - -export default function(history, done) { - const steps = [ - location => { - expect(location).toMatchObject({ - pathname: '/' - }); - - expect(window.location.hash).toBe('#!/'); - - history.push('/home?the=query#the-hash'); - }, - location => { - expect(location).toMatchObject({ - pathname: '/home', - search: '?the=query', - hash: '#the-hash' - }); - - expect(window.location.hash).toBe('#!/home?the=query#the-hash'); - - history.goBack(); - }, - location => { - expect(location).toMatchObject({ - pathname: '/' - }); - - expect(window.location.hash).toBe('#!/'); - - history.goForward(); - }, - location => { - expect(location).toMatchObject({ - pathname: '/home', - search: '?the=query', - hash: '#the-hash' - }); - - expect(window.location.hash).toBe('#!/home?the=query#the-hash'); - } - ]; - - execSteps(steps, history, done); -} diff --git a/modules/__tests__/TestSequences/InitialLocationNoKey.js b/modules/__tests__/TestSequences/InitialLocationDefaultKey.js similarity index 65% rename from modules/__tests__/TestSequences/InitialLocationNoKey.js rename to modules/__tests__/TestSequences/InitialLocationDefaultKey.js index 8c79d34b2..eb13fee25 100644 --- a/modules/__tests__/TestSequences/InitialLocationNoKey.js +++ b/modules/__tests__/TestSequences/InitialLocationDefaultKey.js @@ -3,9 +3,9 @@ import expect from 'expect'; import execSteps from './execSteps.js'; export default function(history, done) { - const steps = [ - location => { - expect(location.key).toBeFalsy(); + let steps = [ + ({ location }) => { + expect(location.key).toBe('default'); } ]; diff --git a/modules/__tests__/TestSequences/InitialLocationHasKey.js b/modules/__tests__/TestSequences/InitialLocationHasKey.js index e9a22b62a..f506fb5b2 100644 --- a/modules/__tests__/TestSequences/InitialLocationHasKey.js +++ b/modules/__tests__/TestSequences/InitialLocationHasKey.js @@ -3,8 +3,8 @@ import expect from 'expect'; import execSteps from './execSteps.js'; export default function(history, done) { - const steps = [ - location => { + let steps = [ + ({ location }) => { expect(location.key).toBeTruthy(); } ]; diff --git a/modules/__tests__/TestSequences/Listen.js b/modules/__tests__/TestSequences/Listen.js index 84393eb68..ddc6c4998 100644 --- a/modules/__tests__/TestSequences/Listen.js +++ b/modules/__tests__/TestSequences/Listen.js @@ -2,8 +2,8 @@ import expect from 'expect'; import mock from 'jest-mock'; export default function(history, done) { - const spy = mock.fn(); - const unlisten = history.listen(spy); + let spy = mock.fn(); + let unlisten = history.listen(spy); expect(spy).not.toHaveBeenCalled(); diff --git a/modules/__tests__/TestSequences/LocationPathnameAlwaysSame.js b/modules/__tests__/TestSequences/LocationPathnameAlwaysSame.js index 48facbc6d..5a6158fef 100644 --- a/modules/__tests__/TestSequences/LocationPathnameAlwaysSame.js +++ b/modules/__tests__/TestSequences/LocationPathnameAlwaysSame.js @@ -3,39 +3,39 @@ import expect from 'expect'; import execSteps from './execSteps.js'; export default function(history, done) { - const steps = [ + let steps = [ () => { // encoded string - const pathname = '/%E6%AD%B4%E5%8F%B2'; + let pathname = '/%E6%AD%B4%E5%8F%B2'; history.replace(pathname); }, - location => { + ({ location }) => { expect(location).toMatchObject({ - pathname: '/歴史' + pathname: '/%E6%AD%B4%E5%8F%B2' }); // encoded object - const pathname = '/%E6%AD%B4%E5%8F%B2'; + let pathname = '/%E6%AD%B4%E5%8F%B2'; history.replace({ pathname }); }, - location => { + ({ location }) => { expect(location).toMatchObject({ - pathname: '/歴史' + pathname: '/%E6%AD%B4%E5%8F%B2' }); // unencoded string - const pathname = '/歴史'; + let pathname = '/歴史'; history.replace(pathname); }, - location => { + ({ location }) => { expect(location).toMatchObject({ - pathname: '/歴史' + pathname: '/%E6%AD%B4%E5%8F%B2' }); // unencoded object - const pathname = '/歴史'; + let pathname = '/歴史'; history.replace({ pathname }); }, - location => { + ({ location }) => { expect(location).toMatchObject({ - pathname: '/歴史' + pathname: '/%E6%AD%B4%E5%8F%B2' }); } ]; diff --git a/modules/__tests__/TestSequences/NoslashHashPathCoding.js b/modules/__tests__/TestSequences/NoslashHashPathCoding.js deleted file mode 100644 index e6df14f3c..000000000 --- a/modules/__tests__/TestSequences/NoslashHashPathCoding.js +++ /dev/null @@ -1,50 +0,0 @@ -import expect from 'expect'; - -import execSteps from './execSteps.js'; - -export default function(history, done) { - const steps = [ - location => { - expect(location).toMatchObject({ - pathname: '/' - }); - - // IE 10+ gives us "#", everyone else gives us "" - expect(window.location.hash).toMatch(/^#?$/); - - history.push('/home?the=query#the-hash'); - }, - location => { - expect(location).toMatchObject({ - pathname: '/home', - search: '?the=query', - hash: '#the-hash' - }); - - expect(window.location.hash).toBe('#home?the=query#the-hash'); - - history.goBack(); - }, - location => { - expect(location).toMatchObject({ - pathname: '/' - }); - - // IE 10+ gives us "#", everyone else gives us "" - expect(window.location.hash).toMatch(/^#?$/); - - history.goForward(); - }, - location => { - expect(location).toMatchObject({ - pathname: '/home', - search: '?the=query', - hash: '#the-hash' - }); - - expect(window.location.hash).toBe('#home?the=query#the-hash'); - } - ]; - - execSteps(steps, history, done); -} diff --git a/modules/__tests__/TestSequences/PushEncodedLocation.js b/modules/__tests__/TestSequences/PushEncodedLocation.js index f71d67109..60a369303 100644 --- a/modules/__tests__/TestSequences/PushEncodedLocation.js +++ b/modules/__tests__/TestSequences/PushEncodedLocation.js @@ -3,21 +3,22 @@ import expect from 'expect'; import execSteps from './execSteps.js'; export default function(history, done) { - const steps = [ - location => { + let steps = [ + ({ location }) => { expect(location).toMatchObject({ pathname: '/' }); - const pathname = '/歴史'; - const search = '?%E3%82%AD%E3%83%BC=%E5%80%A4'; - const hash = '#%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5'; + let pathname = '/歴史'; + let search = '?%E3%82%AD%E3%83%BC=%E5%80%A4'; + let hash = '#%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5'; history.push(pathname + search + hash); }, - (location, action) => { + ({ action, location }) => { expect(action).toBe('PUSH'); expect(location).toMatchObject({ - pathname: '/歴史', + pathname: '/%E6%AD%B4%E5%8F%B2', + // pathname: '/歴史', search: '?%E3%82%AD%E3%83%BC=%E5%80%A4', hash: '#%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5' }); diff --git a/modules/__tests__/TestSequences/PushInvalidPathname.js b/modules/__tests__/TestSequences/PushInvalidPathname.js deleted file mode 100644 index 5024eebbe..000000000 --- a/modules/__tests__/TestSequences/PushInvalidPathname.js +++ /dev/null @@ -1,17 +0,0 @@ -import expect from 'expect'; - -import execSteps from './execSteps'; - -export default function(history, done) { - const steps = [ - () => { - expect(() => { - history.push('/hello%'); - }).toThrow( - 'Pathname "/hello%" could not be decoded. This is likely caused by an invalid percent-encoding.' - ); - } - ]; - - execSteps(steps, history, done); -} diff --git a/modules/__tests__/TestSequences/PushMissingPathname.js b/modules/__tests__/TestSequences/PushMissingPathname.js index 28e7da79e..1f3499afa 100644 --- a/modules/__tests__/TestSequences/PushMissingPathname.js +++ b/modules/__tests__/TestSequences/PushMissingPathname.js @@ -3,15 +3,15 @@ import expect from 'expect'; import execSteps from './execSteps.js'; export default function(history, done) { - const steps = [ - location => { + let steps = [ + ({ location }) => { expect(location).toMatchObject({ pathname: '/' }); history.push('/home?the=query#the-hash'); }, - (location, action) => { + ({ action, location }) => { expect(action).toBe('PUSH'); expect(location).toMatchObject({ pathname: '/home', @@ -21,7 +21,7 @@ export default function(history, done) { history.push('?another=query#another-hash'); }, - (location, action) => { + ({ action, location }) => { expect(action).toBe('PUSH'); expect(location).toMatchObject({ pathname: '/home', diff --git a/modules/__tests__/TestSequences/PushNewLocation.js b/modules/__tests__/TestSequences/PushNewLocation.js index 073f6308e..156bef476 100644 --- a/modules/__tests__/TestSequences/PushNewLocation.js +++ b/modules/__tests__/TestSequences/PushNewLocation.js @@ -3,15 +3,15 @@ import expect from 'expect'; import execSteps from './execSteps.js'; export default function(history, done) { - const steps = [ - location => { + let steps = [ + ({ location }) => { expect(location).toMatchObject({ pathname: '/' }); history.push('/home?the=query#the-hash'); }, - (location, action) => { + ({ action, location }) => { expect(action).toBe('PUSH'); expect(location).toMatchObject({ pathname: '/home', diff --git a/modules/__tests__/TestSequences/PushRelativePathname.js b/modules/__tests__/TestSequences/PushRelativePathname.js index 418bf374c..75850e724 100644 --- a/modules/__tests__/TestSequences/PushRelativePathname.js +++ b/modules/__tests__/TestSequences/PushRelativePathname.js @@ -3,15 +3,15 @@ import expect from 'expect'; import execSteps from './execSteps.js'; export default function(history, done) { - const steps = [ - location => { + let steps = [ + ({ location }) => { expect(location).toMatchObject({ pathname: '/' }); history.push('/the/path?the=query#the-hash'); }, - (location, action) => { + ({ action, location }) => { expect(action).toBe('PUSH'); expect(location).toMatchObject({ pathname: '/the/path', @@ -21,7 +21,7 @@ export default function(history, done) { history.push('../other/path?another=query#another-hash'); }, - (location, action) => { + ({ action, location }) => { expect(action).toBe('PUSH'); expect(location).toMatchObject({ pathname: '/other/path', diff --git a/modules/__tests__/TestSequences/PushRelativePathnameError.js b/modules/__tests__/TestSequences/PushRelativePathnameError.js new file mode 100644 index 000000000..7c8a0759d --- /dev/null +++ b/modules/__tests__/TestSequences/PushRelativePathnameError.js @@ -0,0 +1,31 @@ +import expect from 'expect'; + +import execSteps from './execSteps.js'; + +export default function(history, done) { + let steps = [ + ({ location }) => { + expect(location).toMatchObject({ + pathname: '/' + }); + + history.push('/the/path?the=query#the-hash'); + }, + ({ action, location }) => { + expect(action).toBe('PUSH'); + expect(location).toMatchObject({ + pathname: '/the/path', + search: '?the=query', + hash: '#the-hash' + }); + + try { + history.push('../other/path?another=query#another-hash'); + } catch (error) { + expect(error.message).toMatch(/relative pathnames are not supported/i); + } + } + ]; + + execSteps(steps, history, done); +} diff --git a/modules/__tests__/TestSequences/PushSamePath.js b/modules/__tests__/TestSequences/PushSamePath.js index 07cdc7ea1..765ae661b 100644 --- a/modules/__tests__/TestSequences/PushSamePath.js +++ b/modules/__tests__/TestSequences/PushSamePath.js @@ -3,15 +3,15 @@ import expect from 'expect'; import execSteps from './execSteps.js'; export default function(history, done) { - const steps = [ - location => { + let steps = [ + ({ location }) => { expect(location).toMatchObject({ pathname: '/' }); history.push('/home'); }, - (location, action) => { + ({ action, location }) => { expect(action).toBe('PUSH'); expect(location).toMatchObject({ pathname: '/home' @@ -19,15 +19,15 @@ export default function(history, done) { history.push('/home'); }, - (location, action) => { + ({ action, location }) => { expect(action).toBe('PUSH'); expect(location).toMatchObject({ pathname: '/home' }); - history.goBack(); + history.back(); }, - (location, action) => { + ({ action, location }) => { expect(action).toBe('POP'); expect(location).toMatchObject({ pathname: '/home' diff --git a/modules/__tests__/TestSequences/PushSamePathWarning.js b/modules/__tests__/TestSequences/PushSamePathWarning.js deleted file mode 100644 index df144ef00..000000000 --- a/modules/__tests__/TestSequences/PushSamePathWarning.js +++ /dev/null @@ -1,55 +0,0 @@ -import expect from 'expect'; - -import execSteps from './execSteps.js'; - -export default function(history, done) { - let prevLocation; - - const steps = [ - location => { - expect(location).toMatchObject({ - pathname: '/' - }); - - history.push('/home'); - }, - (location, action) => { - expect(action).toBe('PUSH'); - expect(location).toMatchObject({ - pathname: '/home' - }); - - prevLocation = location; - - history.push('/home'); - }, - (location, action) => { - expect(action).toBe('PUSH'); - expect(location).toMatchObject({ - pathname: '/home' - }); - - // We should get the SAME location object. Nothing - // new was added to the history stack. - expect(location).toBe(prevLocation); - - // We should see a warning message. - expect(warningMessage).toMatch( - 'Hash history cannot PUSH the same path; a new entry will not be added to the history stack' - ); - } - ]; - - let consoleWarn = console.error; // eslint-disable-line no-console - let warningMessage; - - // eslint-disable-next-line no-console - console.warn = message => { - warningMessage = message; - }; - - execSteps(steps, history, (...args) => { - console.warn = consoleWarn; // eslint-disable-line no-console - done(...args); - }); -} diff --git a/modules/__tests__/TestSequences/PushState.js b/modules/__tests__/TestSequences/PushState.js index b22813b74..0d111e61b 100644 --- a/modules/__tests__/TestSequences/PushState.js +++ b/modules/__tests__/TestSequences/PushState.js @@ -3,15 +3,15 @@ import expect from 'expect'; import execSteps from './execSteps.js'; export default function(history, done) { - const steps = [ - location => { + let steps = [ + ({ location }) => { expect(location).toMatchObject({ pathname: '/' }); history.push('/home?the=query#the-hash', { the: 'state' }); }, - (location, action) => { + ({ action, location }) => { expect(action).toBe('PUSH'); expect(location).toMatchObject({ pathname: '/home', diff --git a/modules/__tests__/TestSequences/PushStateWarning.js b/modules/__tests__/TestSequences/PushStateWarning.js deleted file mode 100644 index d19fdab08..000000000 --- a/modules/__tests__/TestSequences/PushStateWarning.js +++ /dev/null @@ -1,40 +0,0 @@ -import expect from 'expect'; - -import execSteps from './execSteps.js'; - -export default function(history, done) { - const steps = [ - location => { - expect(location).toMatchObject({ - pathname: '/' - }); - - history.push('/home', { the: 'state' }); - }, - (location, action) => { - expect(action).toBe('PUSH'); - expect(location).toMatchObject({ - pathname: '/home', - state: undefined - }); - - // We should see a warning message. - expect(warningMessage).toMatch( - 'Hash history cannot push state; it is ignored' - ); - } - ]; - - let consoleWarn = console.warn; // eslint-disable-line no-console - let warningMessage; - - // eslint-disable-next-line no-console - console.warn = message => { - warningMessage = message; - }; - - execSteps(steps, history, (...args) => { - console.warn = consoleWarn; // eslint-disable-line no-console - done(...args); - }); -} diff --git a/modules/__tests__/TestSequences/PushUnicodeLocation.js b/modules/__tests__/TestSequences/PushUnicodeLocation.js index 2bad4645c..8401c2c87 100644 --- a/modules/__tests__/TestSequences/PushUnicodeLocation.js +++ b/modules/__tests__/TestSequences/PushUnicodeLocation.js @@ -3,23 +3,26 @@ import expect from 'expect'; import execSteps from './execSteps.js'; export default function(history, done) { - const steps = [ - location => { + let steps = [ + ({ location }) => { expect(location).toMatchObject({ pathname: '/' }); - const pathname = '/歴史'; - const search = '?キー=値'; - const hash = '#ハッシュ'; + let pathname = '/歴史'; + let search = '?キー=値'; + let hash = '#ハッシュ'; history.push(pathname + search + hash); }, - (location, action) => { + ({ action, location }) => { expect(action).toBe('PUSH'); expect(location).toMatchObject({ - pathname: '/歴史', - search: '?キー=値', - hash: '#ハッシュ' + pathname: '/%E6%AD%B4%E5%8F%B2', + search: '?%E3%82%AD%E3%83%BC=%E5%80%A4', + hash: '#%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5' + // pathname: '/歴史', + // search: '?キー=値', + // hash: '#ハッシュ' }); } ]; diff --git a/modules/__tests__/TestSequences/ReplaceInvalidPathname.js b/modules/__tests__/TestSequences/ReplaceInvalidPathname.js deleted file mode 100644 index 5b85431f7..000000000 --- a/modules/__tests__/TestSequences/ReplaceInvalidPathname.js +++ /dev/null @@ -1,17 +0,0 @@ -import expect from 'expect'; - -import execSteps from './execSteps'; - -export default function(history, done) { - const steps = [ - () => { - expect(() => { - history.replace('/hello%'); - }).toThrow( - 'Pathname "/hello%" could not be decoded. This is likely caused by an invalid percent-encoding.' - ); - } - ]; - - execSteps(steps, history, done); -} diff --git a/modules/__tests__/TestSequences/ReplaceNewLocation.js b/modules/__tests__/TestSequences/ReplaceNewLocation.js index 7fb49c80e..fcfb3c421 100644 --- a/modules/__tests__/TestSequences/ReplaceNewLocation.js +++ b/modules/__tests__/TestSequences/ReplaceNewLocation.js @@ -3,15 +3,15 @@ import expect from 'expect'; import execSteps from './execSteps.js'; export default function(history, done) { - const steps = [ - location => { + let steps = [ + ({ location }) => { expect(location).toMatchObject({ pathname: '/' }); history.replace('/home?the=query#the-hash'); }, - (location, action) => { + ({ action, location }) => { expect(action).toBe('REPLACE'); expect(location).toMatchObject({ pathname: '/home', diff --git a/modules/__tests__/TestSequences/ReplaceSamePath.js b/modules/__tests__/TestSequences/ReplaceSamePath.js index 2dd7ce39c..ffd292f09 100644 --- a/modules/__tests__/TestSequences/ReplaceSamePath.js +++ b/modules/__tests__/TestSequences/ReplaceSamePath.js @@ -5,15 +5,15 @@ import execSteps from './execSteps.js'; export default function(history, done) { let prevLocation; - const steps = [ - location => { + let steps = [ + ({ location }) => { expect(location).toMatchObject({ pathname: '/' }); history.replace('/home'); }, - (location, action) => { + ({ action, location }) => { expect(action).toBe('REPLACE'); expect(location).toMatchObject({ pathname: '/home' @@ -23,7 +23,7 @@ export default function(history, done) { history.replace('/home'); }, - (location, action) => { + ({ action, location }) => { expect(action).toBe('REPLACE'); expect(location).toMatchObject({ pathname: '/home' diff --git a/modules/__tests__/TestSequences/ReplaceState.js b/modules/__tests__/TestSequences/ReplaceState.js index 7f425d79f..7e3e50a61 100644 --- a/modules/__tests__/TestSequences/ReplaceState.js +++ b/modules/__tests__/TestSequences/ReplaceState.js @@ -3,15 +3,15 @@ import expect from 'expect'; import execSteps from './execSteps.js'; export default function(history, done) { - const steps = [ - location => { + let steps = [ + ({ location }) => { expect(location).toMatchObject({ pathname: '/' }); history.replace('/home?the=query#the-hash', { the: 'state' }); }, - (location, action) => { + ({ action, location }) => { expect(action).toBe('REPLACE'); expect(location).toMatchObject({ pathname: '/home', diff --git a/modules/__tests__/TestSequences/ReplaceStateWarning.js b/modules/__tests__/TestSequences/ReplaceStateWarning.js deleted file mode 100644 index a2804abfd..000000000 --- a/modules/__tests__/TestSequences/ReplaceStateWarning.js +++ /dev/null @@ -1,40 +0,0 @@ -import expect from 'expect'; - -import execSteps from './execSteps.js'; - -export default function(history, done) { - const steps = [ - location => { - expect(location).toMatchObject({ - pathname: '/' - }); - - history.replace('/home', { the: 'state' }); - }, - (location, action) => { - expect(action).toBe('REPLACE'); - expect(location).toMatchObject({ - pathname: '/home', - state: undefined - }); - - // We should see a warning message. - expect(warningMessage).toMatch( - 'Hash history cannot replace state; it is ignored' - ); - } - ]; - - let consoleWarn = console.warn; // eslint-disable-line no-console - let warningMessage; - - // eslint-disable-next-line no-console - console.warn = message => { - warningMessage = message; - }; - - execSteps(steps, history, (...args) => { - console.warn = consoleWarn; // eslint-disable-line no-console - done(...args); - }); -} diff --git a/modules/__tests__/TestSequences/ReturnFalseTransitionHook.js b/modules/__tests__/TestSequences/ReturnFalseTransitionHook.js deleted file mode 100644 index d3894fb8a..000000000 --- a/modules/__tests__/TestSequences/ReturnFalseTransitionHook.js +++ /dev/null @@ -1,32 +0,0 @@ -import expect from 'expect'; - -import execSteps from './execSteps.js'; - -export default function(history, done) { - const steps = [ - location => { - expect(location).toMatchObject({ - pathname: '/' - }); - - const unblock = history.block(nextLocation => { - expect(nextLocation).toMatchObject({ - pathname: '/home' - }); - - // Cancel the transition. - return false; - }); - - history.push('/home'); - - expect(history.location).toMatchObject({ - pathname: '/' - }); - - unblock(); - } - ]; - - execSteps(steps, history, done); -} diff --git a/modules/__tests__/TestSequences/SlashHashPathCoding.js b/modules/__tests__/TestSequences/SlashHashPathCoding.js deleted file mode 100644 index 6911d217c..000000000 --- a/modules/__tests__/TestSequences/SlashHashPathCoding.js +++ /dev/null @@ -1,48 +0,0 @@ -import expect from 'expect'; - -import execSteps from './execSteps.js'; - -export default function(history, done) { - const steps = [ - location => { - expect(location).toMatchObject({ - pathname: '/' - }); - - expect(window.location.hash).toBe('#/'); - - history.push('/home?the=query#the-hash'); - }, - location => { - expect(location).toMatchObject({ - pathname: '/home', - search: '?the=query', - hash: '#the-hash' - }); - - expect(window.location.hash).toBe('#/home?the=query#the-hash'); - - history.goBack(); - }, - location => { - expect(location).toMatchObject({ - pathname: '/' - }); - - expect(window.location.hash).toBe('#/'); - - history.goForward(); - }, - location => { - expect(location).toMatchObject({ - pathname: '/home', - search: '?the=query', - hash: '#the-hash' - }); - - expect(window.location.hash).toBe('#/home?the=query#the-hash'); - } - ]; - - execSteps(steps, history, done); -} diff --git a/modules/__tests__/TestSequences/TransitionHookArgs.js b/modules/__tests__/TestSequences/TransitionHookArgs.js deleted file mode 100644 index bc79f020c..000000000 --- a/modules/__tests__/TestSequences/TransitionHookArgs.js +++ /dev/null @@ -1,32 +0,0 @@ -import expect from 'expect'; - -import execSteps from './execSteps.js'; - -export default function(history, done) { - let hookLocation, hookAction; - const steps = [ - location => { - expect(location).toMatchObject({ - pathname: '/' - }); - - history.push('/home'); - }, - (location, action) => { - expect(hookAction).toBe(action); - expect(hookLocation).toBe(location); - } - ]; - - const unblock = history.block((location, action) => { - hookLocation = location; - hookAction = action; - - return 'Are you sure?'; - }); - - execSteps(steps, history, (...args) => { - unblock(); - done(...args); - }); -} diff --git a/modules/__tests__/TestSequences/execSteps.js b/modules/__tests__/TestSequences/execSteps.js index 264560a34..0e3c836a7 100644 --- a/modules/__tests__/TestSequences/execSteps.js +++ b/modules/__tests__/TestSequences/execSteps.js @@ -3,7 +3,7 @@ export default function execSteps(steps, history, done) { unlisten, cleanedUp = false; - const cleanup = (...args) => { + let cleanup = (...args) => { if (!cleanedUp) { cleanedUp = true; unlisten(); @@ -11,9 +11,9 @@ export default function execSteps(steps, history, done) { } }; - const execNextStep = (...args) => { + let execNextStep = (...args) => { try { - const nextStep = steps[index++]; + let nextStep = steps[index++]; if (!nextStep) throw new Error('Test is missing step ' + index); @@ -27,7 +27,11 @@ export default function execSteps(steps, history, done) { if (steps.length) { unlisten = history.listen(execNextStep); - execNextStep(history.location); + + execNextStep({ + action: history.action, + location: history.location + }); } else { done(); } diff --git a/modules/__tests__/TestSequences/index.js b/modules/__tests__/TestSequences/index.js deleted file mode 100644 index e48e6e738..000000000 --- a/modules/__tests__/TestSequences/index.js +++ /dev/null @@ -1,46 +0,0 @@ -export { - default as BackButtonTransitionHook -} from './BackButtonTransitionHook.js'; -export { default as BlockEverything } from './BlockEverything.js'; -export { - default as BlockPopWithoutListening -} from './BlockPopWithoutListening.js'; -export { default as DenyPush } from './DenyPush.js'; -export { default as DenyGoBack } from './DenyGoBack.js'; -export { default as DenyGoForward } from './DenyGoForward.js'; -export { - default as EncodedReservedCharacters -} from './EncodedReservedCharacters.js'; -export { default as GoBack } from './GoBack.js'; -export { default as GoForward } from './GoForward.js'; -export { default as HashbangHashPathCoding } from './HashbangHashPathCoding.js'; -export { - default as HashChangeTransitionHook -} from './HashChangeTransitionHook.js'; -export { default as InitialLocationNoKey } from './InitialLocationNoKey.js'; -export { default as InitialLocationHasKey } from './InitialLocationHasKey.js'; -export { default as Listen } from './Listen.js'; -export { - default as LocationPathnameAlwaysSame -} from './LocationPathnameAlwaysSame.js'; -export { default as NoslashHashPathCoding } from './NoslashHashPathCoding.js'; -export { default as PushEncodedLocation } from './PushEncodedLocation.js'; -export { default as PushInvalidPathname } from './PushInvalidPathname.js'; -export { default as PushNewLocation } from './PushNewLocation.js'; -export { default as PushMissingPathname } from './PushMissingPathname.js'; -export { default as PushSamePath } from './PushSamePath.js'; -export { default as PushSamePathWarning } from './PushSamePathWarning.js'; -export { default as PushState } from './PushState.js'; -export { default as PushStateWarning } from './PushStateWarning.js'; -export { default as PushRelativePathname } from './PushRelativePathname.js'; -export { default as PushUnicodeLocation } from './PushUnicodeLocation.js'; -export { default as ReplaceInvalidPathname } from './ReplaceInvalidPathname.js'; -export { default as ReplaceNewLocation } from './ReplaceNewLocation.js'; -export { default as ReplaceSamePath } from './ReplaceSamePath.js'; -export { default as ReplaceState } from './ReplaceState.js'; -export { default as ReplaceStateWarning } from './ReplaceStateWarning.js'; -export { - default as ReturnFalseTransitionHook -} from './ReturnFalseTransitionHook.js'; -export { default as SlashHashPathCoding } from './SlashHashPathCoding.js'; -export { default as TransitionHookArgs } from './TransitionHookArgs.js'; diff --git a/modules/__tests__/createBrowserHistory-test.js b/modules/__tests__/createBrowserHistory-test.js new file mode 100644 index 000000000..3d9bf02b3 --- /dev/null +++ b/modules/__tests__/createBrowserHistory-test.js @@ -0,0 +1,163 @@ +import expect from 'expect'; +import { createBrowserHistory } from 'history'; + +import InitialLocationDefaultKey from './TestSequences/InitialLocationDefaultKey.js'; +import Listen from './TestSequences/Listen.js'; +import PushNewLocation from './TestSequences/PushNewLocation.js'; +import PushSamePath from './TestSequences/PushSamePath.js'; +import PushState from './TestSequences/PushState.js'; +import PushMissingPathname from './TestSequences/PushMissingPathname.js'; +import PushRelativePathname from './TestSequences/PushRelativePathname.js'; +import PushUnicodeLocation from './TestSequences/PushUnicodeLocation.js'; +import PushEncodedLocation from './TestSequences/PushEncodedLocation.js'; +import ReplaceNewLocation from './TestSequences/ReplaceNewLocation.js'; +import ReplaceSamePath from './TestSequences/ReplaceSamePath.js'; +import ReplaceState from './TestSequences/ReplaceState.js'; +import LocationPathnameAlwaysSame from './TestSequences/LocationPathnameAlwaysSame.js'; +import EncodedReservedCharacters from './TestSequences/EncodedReservedCharacters.js'; +import GoBack from './TestSequences/GoBack.js'; +import GoForward from './TestSequences/GoForward.js'; +import BlockEverything from './TestSequences/BlockEverything.js'; +import BlockPopWithoutListening from './TestSequences/BlockPopWithoutListening.js'; + +describe('a browser history', () => { + let history; + beforeEach(() => { + if (window.location.pathname !== '/') { + window.history.replaceState(null, null, '/'); + } + history = createBrowserHistory(); + }); + + it('knows how to create hrefs', () => { + const href = history.createHref({ + pathname: '/the/path', + search: '?the=query', + hash: '#the-hash' + }); + + expect(href).toEqual('/the/path?the=query#the-hash'); + }); + + it('does not encode the generated path', () => { + // encoded + const encodedHref = history.createHref({ + pathname: '/%23abc' + }); + // unencoded + const unencodedHref = history.createHref({ + pathname: '/#abc' + }); + + expect(encodedHref).toEqual('/%23abc'); + expect(unencodedHref).toEqual('/#abc'); + }); + + describe('listen', () => { + it('does not immediately call listeners', done => { + Listen(history, done); + }); + }); + + describe('the initial location', () => { + it('has the "default" key', done => { + InitialLocationDefaultKey(history, done); + }); + }); + + describe('push a new path', () => { + it('calls change listeners with the new location', done => { + PushNewLocation(history, done); + }); + }); + + describe('push the same path', () => { + it('calls change listeners with the new location', done => { + PushSamePath(history, done); + }); + }); + + describe('push state', () => { + it('calls change listeners with the new location', done => { + PushState(history, done); + }); + }); + + describe.skip('push with no pathname', () => { + it('calls change listeners with the normalized location', done => { + PushMissingPathname(history, done); + }); + }); + + describe('push with a relative pathname', () => { + it('calls change listeners with the normalized location', done => { + PushRelativePathname(history, done); + }); + }); + + describe('push with a unicode path string', () => { + it('creates a location with encoded properties', done => { + PushUnicodeLocation(history, done); + }); + }); + + describe('push with an encoded path string', () => { + it('creates a location with encoded pathname', done => { + PushEncodedLocation(history, done); + }); + }); + + describe('replace a new path', () => { + it('calls change listeners with the new location', done => { + ReplaceNewLocation(history, done); + }); + }); + + describe('replace the same path', () => { + it('calls change listeners with the new location', done => { + ReplaceSamePath(history, done); + }); + }); + + describe('replace state', () => { + it('calls change listeners with the new location', done => { + ReplaceState(history, done); + }); + }); + + describe('location created by encoded and unencoded pathname', () => { + it('produces the same location.pathname', done => { + LocationPathnameAlwaysSame(history, done); + }); + }); + + describe('location created with encoded/unencoded reserved characters', () => { + it('produces different location objects', done => { + EncodedReservedCharacters(history, done); + }); + }); + + describe('back', () => { + it('calls change listeners with the previous location', done => { + GoBack(history, done); + }); + }); + + describe('forward', () => { + it('calls change listeners with the next location', done => { + GoForward(history, done); + }); + }); + + describe('block', () => { + it('blocks all transitions', done => { + BlockEverything(history, done); + }); + }); + + describe('block a POP without listening', () => { + it('receives the next ({ action, location })', done => { + BlockPopWithoutListening(history, done); + }); + }); +}); diff --git a/modules/__tests__/HashHistory-base-test.js b/modules/__tests__/createHashHistory-base-test.js similarity index 91% rename from modules/__tests__/HashHistory-base-test.js rename to modules/__tests__/createHashHistory-base-test.js index b3bd8b924..dbbfa3ac0 100644 --- a/modules/__tests__/HashHistory-base-test.js +++ b/modules/__tests__/createHashHistory-base-test.js @@ -4,6 +4,10 @@ import { createHashHistory } from 'history'; describe('a hash history on a page with a tag', () => { let history, base; beforeEach(() => { + if (window.location.hash !== '#/') { + window.location.hash = '/'; + } + base = document.createElement('base'); base.setAttribute('href', '/prefix'); diff --git a/modules/__tests__/createHashHistory-test.js b/modules/__tests__/createHashHistory-test.js new file mode 100644 index 000000000..cd71f98c9 --- /dev/null +++ b/modules/__tests__/createHashHistory-test.js @@ -0,0 +1,167 @@ +import expect from 'expect'; +import { createHashHistory } from 'history'; + +import Listen from './TestSequences/Listen.js'; +import InitialLocationDefaultKey from './TestSequences/InitialLocationDefaultKey.js'; +import PushNewLocation from './TestSequences/PushNewLocation.js'; +import PushSamePath from './TestSequences/PushSamePath.js'; +import PushState from './TestSequences/PushState.js'; +import PushMissingPathname from './TestSequences/PushMissingPathname.js'; +import PushRelativePathnameError from './TestSequences/PushRelativePathnameError.js'; +import PushUnicodeLocation from './TestSequences/PushUnicodeLocation.js'; +import PushEncodedLocation from './TestSequences/PushEncodedLocation.js'; +import ReplaceNewLocation from './TestSequences/ReplaceNewLocation.js'; +import ReplaceSamePath from './TestSequences/ReplaceSamePath.js'; +import ReplaceState from './TestSequences/ReplaceState.js'; +import LocationPathnameAlwaysSame from './TestSequences/LocationPathnameAlwaysSame.js'; +import EncodedReservedCharacters from './TestSequences/EncodedReservedCharacters.js'; +import GoBack from './TestSequences/GoBack.js'; +import GoForward from './TestSequences/GoForward.js'; +import BlockEverything from './TestSequences/BlockEverything.js'; +import BlockPopWithoutListening from './TestSequences/BlockPopWithoutListening.js'; + +// TODO: Do we still need this? +// const canGoWithoutReload = window.navigator.userAgent.indexOf('Firefox') === -1; +// const describeGo = canGoWithoutReload ? describe : describe.skip; + +describe('a hash history', () => { + let history; + beforeEach(() => { + if (window.location.hash !== '#/') { + window.location.hash = '/'; + } + history = createHashHistory(); + }); + + it('knows how to create hrefs', () => { + const href = history.createHref({ + pathname: '/the/path', + search: '?the=query', + hash: '#the-hash' + }); + + expect(href).toEqual('#/the/path?the=query#the-hash'); + }); + + it('does not encode the generated path', () => { + // encoded + const encodedHref = history.createHref({ + pathname: '/%23abc' + }); + // unencoded + const unencodedHref = history.createHref({ + pathname: '/#abc' + }); + + expect(encodedHref).toEqual('#/%23abc'); + expect(unencodedHref).toEqual('#/#abc'); + }); + + describe('listen', () => { + it('does not immediately call listeners', done => { + Listen(history, done); + }); + }); + + describe('the initial location', () => { + it('has the "default" key', done => { + InitialLocationDefaultKey(history, done); + }); + }); + + describe('push a new path', () => { + it('calls change listeners with the new location', done => { + PushNewLocation(history, done); + }); + }); + + describe('push the same path', () => { + it('calls change listeners with the new location', done => { + PushSamePath(history, done); + }); + }); + + describe('push state', () => { + it('calls change listeners with the new location', done => { + PushState(history, done); + }); + }); + + describe.skip('push with no pathname', () => { + it('calls change listeners with the normalized location', done => { + PushMissingPathname(history, done); + }); + }); + + describe('push with a relative pathname', () => { + it('throws an error', done => { + PushRelativePathnameError(history, done); + }); + }); + + describe('push with a unicode path string', () => { + it('creates a location with decoded properties', done => { + PushUnicodeLocation(history, done); + }); + }); + + describe('push with an encoded path string', () => { + it('creates a location object with encoded pathname', done => { + PushEncodedLocation(history, done); + }); + }); + + describe('replace a new path', () => { + it('calls change listeners with the new location', done => { + ReplaceNewLocation(history, done); + }); + }); + + describe('replace the same path', () => { + it('calls change listeners with the new location', done => { + ReplaceSamePath(history, done); + }); + }); + + describe('replace state', () => { + it('calls change listeners with the new location', done => { + ReplaceState(history, done); + }); + }); + + describe('location created by encoded and unencoded pathname', () => { + it('produces the same location.pathname', done => { + LocationPathnameAlwaysSame(history, done); + }); + }); + + describe('location created with encoded/unencoded reserved characters', () => { + it('produces different location objects', done => { + EncodedReservedCharacters(history, done); + }); + }); + + describe('back', () => { + it('calls change listeners with the previous location', done => { + GoBack(history, done); + }); + }); + + describe('forward', () => { + it('calls change listeners with the next location', done => { + GoForward(history, done); + }); + }); + + describe('block', () => { + it('blocks all transitions', done => { + BlockEverything(history, done); + }); + }); + + describe('block a POP without listening', () => { + it('receives the next location and action as arguments', done => { + BlockPopWithoutListening(history, done); + }); + }); +}); diff --git a/modules/__tests__/createLocation-test.js b/modules/__tests__/createLocation-test.js deleted file mode 100644 index 27e084265..000000000 --- a/modules/__tests__/createLocation-test.js +++ /dev/null @@ -1,144 +0,0 @@ -import expect from 'expect'; -import { createLocation } from 'history'; - -describe('createLocation', () => { - describe('with a full path', () => { - describe('given as a string', () => { - it('has the correct properties', () => { - expect(createLocation('/the/path?the=query#the-hash')).toMatchObject({ - pathname: '/the/path', - search: '?the=query', - hash: '#the-hash' - }); - }); - }); - - describe('given as an object', () => { - it('has the correct properties', () => { - expect( - createLocation({ - pathname: '/the/path', - search: '?the=query', - hash: '#the-hash' - }) - ).toMatchObject({ - pathname: '/the/path', - search: '?the=query', - hash: '#the-hash' - }); - }); - }); - }); - - describe('with a relative path', () => { - describe('given as a string', () => { - it('has the correct properties', () => { - expect(createLocation('the/path?the=query#the-hash')).toMatchObject({ - pathname: 'the/path', - search: '?the=query', - hash: '#the-hash' - }); - }); - }); - - describe('given as an object', () => { - it('has the correct properties', () => { - expect( - createLocation({ - pathname: 'the/path', - search: '?the=query', - hash: '#the-hash' - }) - ).toMatchObject({ - pathname: 'the/path', - search: '?the=query', - hash: '#the-hash' - }); - }); - }); - }); - - describe('with a path with no pathname', () => { - describe('given as a string', () => { - it('has the correct properties', () => { - expect(createLocation('?the=query#the-hash')).toMatchObject({ - pathname: '/', - search: '?the=query', - hash: '#the-hash' - }); - }); - }); - - describe('given as an object', () => { - it('has the correct properties', () => { - expect( - createLocation({ search: '?the=query', hash: '#the-hash' }) - ).toMatchObject({ - pathname: '/', - search: '?the=query', - hash: '#the-hash' - }); - }); - }); - }); - - describe('with a path with no search', () => { - describe('given as a string', () => { - it('has the correct properties', () => { - expect(createLocation('/the/path#the-hash')).toMatchObject({ - pathname: '/the/path', - search: '', - hash: '#the-hash' - }); - }); - }); - - describe('given as an object', () => { - it('has the correct properties', () => { - expect( - createLocation({ pathname: '/the/path', hash: '#the-hash' }) - ).toMatchObject({ - pathname: '/the/path', - search: '', - hash: '#the-hash' - }); - }); - }); - }); - - describe('with a path with no hash', () => { - describe('given as a string', () => { - it('has the correct properties', () => { - expect(createLocation('/the/path?the=query')).toMatchObject({ - pathname: '/the/path', - search: '?the=query', - hash: '' - }); - }); - }); - - describe('given as an object', () => { - it('has the correct properties', () => { - expect( - createLocation({ pathname: '/the/path', search: '?the=query' }) - ).toMatchObject({ - pathname: '/the/path', - search: '?the=query', - hash: '' - }); - }); - }); - }); - - describe('key', () => { - it('has a key property if a key is provided', () => { - const location = createLocation('/the/path', undefined, 'key'); - expect(Object.keys(location)).toContain('key'); - }); - - it('has no key property if no key is provided', () => { - const location = createLocation('/the/path'); - expect(Object.keys(location)).not.toContain('key'); - }); - }); -}); diff --git a/modules/__tests__/createMemoryHistory-test.js b/modules/__tests__/createMemoryHistory-test.js new file mode 100644 index 000000000..25d69e313 --- /dev/null +++ b/modules/__tests__/createMemoryHistory-test.js @@ -0,0 +1,139 @@ +import expect from 'expect'; +import { createMemoryHistory } from 'history'; + +import Listen from './TestSequences/Listen.js'; +import InitialLocationHasKey from './TestSequences/InitialLocationHasKey.js'; +import PushNewLocation from './TestSequences/PushNewLocation.js'; +import PushSamePath from './TestSequences/PushSamePath.js'; +import PushState from './TestSequences/PushState.js'; +import PushMissingPathname from './TestSequences/PushMissingPathname.js'; +import PushRelativePathnameError from './TestSequences/PushRelativePathnameError.js'; +import ReplaceNewLocation from './TestSequences/ReplaceNewLocation.js'; +import ReplaceSamePath from './TestSequences/ReplaceSamePath.js'; +import ReplaceState from './TestSequences/ReplaceState.js'; +import EncodedReservedCharacters from './TestSequences/EncodedReservedCharacters.js'; +import GoBack from './TestSequences/GoBack.js'; +import GoForward from './TestSequences/GoForward.js'; +import BlockEverything from './TestSequences/BlockEverything.js'; +import BlockPopWithoutListening from './TestSequences/BlockPopWithoutListening.js'; + +describe('a memory history', () => { + let history; + beforeEach(() => { + history = createMemoryHistory(); + }); + + it('knows how to create hrefs', () => { + const href = history.createHref({ + pathname: '/the/path', + search: '?the=query', + hash: '#the-hash' + }); + + expect(href).toEqual('/the/path?the=query#the-hash'); + }); + + it('does not encode the generated path', () => { + // encoded + const encodedHref = history.createHref({ + pathname: '/%23abc' + }); + // unencoded + const unencodedHref = history.createHref({ + pathname: '/#abc' + }); + + expect(encodedHref).toEqual('/%23abc'); + expect(unencodedHref).toEqual('/#abc'); + }); + + describe('listen', () => { + it('does not immediately call listeners', done => { + Listen(history, done); + }); + }); + + describe('the initial location', () => { + it('has a key', done => { + InitialLocationHasKey(history, done); + }); + }); + + describe('push a new path', () => { + it('calls change listeners with the new location', done => { + PushNewLocation(history, done); + }); + }); + + describe('push the same path', () => { + it('calls change listeners with the new location', done => { + PushSamePath(history, done); + }); + }); + + describe('push state', () => { + it('calls change listeners with the new location', done => { + PushState(history, done); + }); + }); + + describe.skip('push with no pathname', () => { + it('calls change listeners with the normalized location', done => { + PushMissingPathname(history, done); + }); + }); + + describe('push with a relative pathname', () => { + it('throws an error', done => { + PushRelativePathnameError(history, done); + }); + }); + + describe('replace a new path', () => { + it('calls change listeners with the new location', done => { + ReplaceNewLocation(history, done); + }); + }); + + describe('replace the same path', () => { + it('calls change listeners with the new location', done => { + ReplaceSamePath(history, done); + }); + }); + + describe('replace state', () => { + it('calls change listeners with the new location', done => { + ReplaceState(history, done); + }); + }); + + describe('location created with encoded/unencoded reserved characters', () => { + it('produces different location objects', done => { + EncodedReservedCharacters(history, done); + }); + }); + + describe('back', () => { + it('calls change listeners with the previous location', done => { + GoBack(history, done); + }); + }); + + describe('forward', () => { + it('calls change listeners with the next location', done => { + GoForward(history, done); + }); + }); + + describe('block', () => { + it('blocks all transitions', done => { + BlockEverything(history, done); + }); + }); + + describe('block a POP without listening', () => { + it('receives the next location and action as arguments', done => { + BlockPopWithoutListening(history, done); + }); + }); +}); diff --git a/modules/createBrowserHistory.js b/modules/createBrowserHistory.js deleted file mode 100644 index dd1983dc0..000000000 --- a/modules/createBrowserHistory.js +++ /dev/null @@ -1,329 +0,0 @@ -import { createLocation } from './LocationUtils.js'; -import { - addLeadingSlash, - stripTrailingSlash, - hasBasename, - stripBasename, - createPath -} from './PathUtils.js'; -import createTransitionManager from './createTransitionManager.js'; -import { - canUseDOM, - getConfirmation, - supportsHistory, - supportsPopStateOnHashChange, - isExtraneousPopstateEvent -} from './DOMUtils.js'; -import invariant from './invariant.js'; -import warning from './warning.js'; - -const PopStateEvent = 'popstate'; -const HashChangeEvent = 'hashchange'; - -function getHistoryState() { - try { - return window.history.state || {}; - } catch (e) { - // IE 11 sometimes throws when accessing window.history.state - // See https://github.com/ReactTraining/history/pull/289 - return {}; - } -} - -/** - * Creates a history object that uses the HTML5 history API including - * pushState, replaceState, and the popstate event. - */ -function createBrowserHistory(props = {}) { - invariant(canUseDOM, 'Browser history needs a DOM'); - - const globalHistory = window.history; - const canUseHistory = supportsHistory(); - const needsHashChangeListener = !supportsPopStateOnHashChange(); - - const { - forceRefresh = false, - getUserConfirmation = getConfirmation, - keyLength = 6 - } = props; - const basename = props.basename - ? stripTrailingSlash(addLeadingSlash(props.basename)) - : ''; - - function getDOMLocation(historyState) { - const { key, state } = historyState || {}; - const { pathname, search, hash } = window.location; - - let path = pathname + search + hash; - - warning( - !basename || hasBasename(path, basename), - 'You are attempting to use a basename on a page whose URL path does not begin ' + - 'with the basename. Expected path "' + - path + - '" to begin with "' + - basename + - '".' - ); - - if (basename) path = stripBasename(path, basename); - - return createLocation(path, state, key); - } - - function createKey() { - return Math.random() - .toString(36) - .substr(2, keyLength); - } - - const transitionManager = createTransitionManager(); - - function setState(nextState) { - Object.assign(history, nextState); - history.length = globalHistory.length; - transitionManager.notifyListeners(history.location, history.action); - } - - function handlePopState(event) { - // Ignore extraneous popstate events in WebKit. - if (isExtraneousPopstateEvent(event)) return; - handlePop(getDOMLocation(event.state)); - } - - function handleHashChange() { - handlePop(getDOMLocation(getHistoryState())); - } - - let forceNextPop = false; - - function handlePop(location) { - if (forceNextPop) { - forceNextPop = false; - setState(); - } else { - const action = 'POP'; - - transitionManager.confirmTransitionTo( - location, - action, - getUserConfirmation, - ok => { - if (ok) { - setState({ action, location }); - } else { - revertPop(location); - } - } - ); - } - } - - function revertPop(fromLocation) { - const toLocation = history.location; - - // TODO: We could probably make this more reliable by - // keeping a list of keys we've seen in sessionStorage. - // Instead, we just default to 0 for keys we don't know. - - let toIndex = allKeys.indexOf(toLocation.key); - - if (toIndex === -1) toIndex = 0; - - let fromIndex = allKeys.indexOf(fromLocation.key); - - if (fromIndex === -1) fromIndex = 0; - - const delta = toIndex - fromIndex; - - if (delta) { - forceNextPop = true; - go(delta); - } - } - - const initialLocation = getDOMLocation(getHistoryState()); - let allKeys = [initialLocation.key]; - - // Public interface - - function createHref(location) { - return basename + createPath(location); - } - - function push(path, state) { - warning( - !( - typeof path === 'object' && - path.state !== undefined && - state !== undefined - ), - 'You should avoid providing a 2nd state argument to push when the 1st ' + - 'argument is a location-like object that already has state; it is ignored' - ); - - const action = 'PUSH'; - const location = createLocation(path, state, createKey(), history.location); - - transitionManager.confirmTransitionTo( - location, - action, - getUserConfirmation, - ok => { - if (!ok) return; - - const href = createHref(location); - const { key, state } = location; - - if (canUseHistory) { - globalHistory.pushState({ key, state }, null, href); - - if (forceRefresh) { - window.location.href = href; - } else { - const prevIndex = allKeys.indexOf(history.location.key); - const nextKeys = allKeys.slice(0, prevIndex + 1); - - nextKeys.push(location.key); - allKeys = nextKeys; - - setState({ action, location }); - } - } else { - warning( - state === undefined, - 'Browser history cannot push state in browsers that do not support HTML5 history' - ); - - window.location.href = href; - } - } - ); - } - - function replace(path, state) { - warning( - !( - typeof path === 'object' && - path.state !== undefined && - state !== undefined - ), - 'You should avoid providing a 2nd state argument to replace when the 1st ' + - 'argument is a location-like object that already has state; it is ignored' - ); - - const action = 'REPLACE'; - const location = createLocation(path, state, createKey(), history.location); - - transitionManager.confirmTransitionTo( - location, - action, - getUserConfirmation, - ok => { - if (!ok) return; - - const href = createHref(location); - const { key, state } = location; - - if (canUseHistory) { - globalHistory.replaceState({ key, state }, null, href); - - if (forceRefresh) { - window.location.replace(href); - } else { - const prevIndex = allKeys.indexOf(history.location.key); - - if (prevIndex !== -1) allKeys[prevIndex] = location.key; - - setState({ action, location }); - } - } else { - warning( - state === undefined, - 'Browser history cannot replace state in browsers that do not support HTML5 history' - ); - - window.location.replace(href); - } - } - ); - } - - function go(n) { - globalHistory.go(n); - } - - function goBack() { - go(-1); - } - - function goForward() { - go(1); - } - - let listenerCount = 0; - - function checkDOMListeners(delta) { - listenerCount += delta; - - if (listenerCount === 1 && delta === 1) { - window.addEventListener(PopStateEvent, handlePopState); - - if (needsHashChangeListener) - window.addEventListener(HashChangeEvent, handleHashChange); - } else if (listenerCount === 0) { - window.removeEventListener(PopStateEvent, handlePopState); - - if (needsHashChangeListener) - window.removeEventListener(HashChangeEvent, handleHashChange); - } - } - - let isBlocked = false; - - function block(prompt = false) { - const unblock = transitionManager.setPrompt(prompt); - - if (!isBlocked) { - checkDOMListeners(1); - isBlocked = true; - } - - return () => { - if (isBlocked) { - isBlocked = false; - checkDOMListeners(-1); - } - - return unblock(); - }; - } - - function listen(listener) { - const unlisten = transitionManager.appendListener(listener); - checkDOMListeners(1); - - return () => { - checkDOMListeners(-1); - unlisten(); - }; - } - - const history = { - length: globalHistory.length, - action: 'POP', - location: initialLocation, - createHref, - push, - replace, - go, - goBack, - goForward, - block, - listen - }; - - return history; -} - -export default createBrowserHistory; diff --git a/modules/createHashHistory.js b/modules/createHashHistory.js deleted file mode 100644 index 6c29725b4..000000000 --- a/modules/createHashHistory.js +++ /dev/null @@ -1,361 +0,0 @@ -import { createLocation } from './LocationUtils.js'; -import { - addLeadingSlash, - stripLeadingSlash, - stripTrailingSlash, - hasBasename, - stripBasename, - createPath -} from './PathUtils.js'; -import createTransitionManager from './createTransitionManager.js'; -import { - canUseDOM, - getConfirmation, - supportsGoWithoutReloadUsingHash -} from './DOMUtils.js'; -import invariant from './invariant.js'; -import warning from './warning.js'; - -const HashChangeEvent = 'hashchange'; - -const HashPathCoders = { - hashbang: { - encodePath: path => - path.charAt(0) === '!' ? path : '!/' + stripLeadingSlash(path), - decodePath: path => (path.charAt(0) === '!' ? path.substr(1) : path) - }, - noslash: { - encodePath: stripLeadingSlash, - decodePath: addLeadingSlash - }, - slash: { - encodePath: addLeadingSlash, - decodePath: addLeadingSlash - } -}; - -function stripHash(url) { - const hashIndex = url.indexOf('#'); - return hashIndex === -1 ? url : url.slice(0, hashIndex); -} - -function getHashPath() { - // We can't use window.location.hash here because it's not - // consistent across browsers - Firefox will pre-decode it! - const href = window.location.href; - const hashIndex = href.indexOf('#'); - return hashIndex === -1 ? '' : href.substring(hashIndex + 1); -} - -function pushHashPath(path) { - window.location.hash = path; -} - -function replaceHashPath(path) { - window.location.replace(stripHash(window.location.href) + '#' + path); -} - -function createHashHistory(props = {}) { - invariant(canUseDOM, 'Hash history needs a DOM'); - - const globalHistory = window.history; - const canGoWithoutReload = supportsGoWithoutReloadUsingHash(); - - const { getUserConfirmation = getConfirmation, hashType = 'slash' } = props; - const basename = props.basename - ? stripTrailingSlash(addLeadingSlash(props.basename)) - : ''; - - const { encodePath, decodePath } = HashPathCoders[hashType]; - - function getDOMLocation() { - let path = decodePath(getHashPath()); - - warning( - !basename || hasBasename(path, basename), - 'You are attempting to use a basename on a page whose URL path does not begin ' + - 'with the basename. Expected path "' + - path + - '" to begin with "' + - basename + - '".' - ); - - if (basename) path = stripBasename(path, basename); - - return createLocation(path); - } - - const transitionManager = createTransitionManager(); - - function setState(nextState) { - Object.assign(history, nextState); - history.length = globalHistory.length; - transitionManager.notifyListeners(history.location, history.action); - } - - let forceNextPop = false; - let ignorePath = null; - - function locationsAreEqual(a, b) { - return ( - a.pathname === b.pathname && a.search === b.search && a.hash === b.hash - ); - } - - function handleHashChange() { - const path = getHashPath(); - const encodedPath = encodePath(path); - - if (path !== encodedPath) { - // Ensure we always have a properly-encoded hash. - replaceHashPath(encodedPath); - } else { - const location = getDOMLocation(); - const prevLocation = history.location; - - if (!forceNextPop && locationsAreEqual(prevLocation, location)) return; // A hashchange doesn't always == location change. - - if (ignorePath === createPath(location)) return; // Ignore this change; we already setState in push/replace. - - ignorePath = null; - - handlePop(location); - } - } - - function handlePop(location) { - if (forceNextPop) { - forceNextPop = false; - setState(); - } else { - const action = 'POP'; - - transitionManager.confirmTransitionTo( - location, - action, - getUserConfirmation, - ok => { - if (ok) { - setState({ action, location }); - } else { - revertPop(location); - } - } - ); - } - } - - function revertPop(fromLocation) { - const toLocation = history.location; - - // TODO: We could probably make this more reliable by - // keeping a list of paths we've seen in sessionStorage. - // Instead, we just default to 0 for paths we don't know. - - let toIndex = allPaths.lastIndexOf(createPath(toLocation)); - - if (toIndex === -1) toIndex = 0; - - let fromIndex = allPaths.lastIndexOf(createPath(fromLocation)); - - if (fromIndex === -1) fromIndex = 0; - - const delta = toIndex - fromIndex; - - if (delta) { - forceNextPop = true; - go(delta); - } - } - - // Ensure the hash is encoded properly before doing anything else. - const path = getHashPath(); - const encodedPath = encodePath(path); - - if (path !== encodedPath) replaceHashPath(encodedPath); - - const initialLocation = getDOMLocation(); - let allPaths = [createPath(initialLocation)]; - - // Public interface - - function createHref(location) { - const baseTag = document.querySelector('base'); - let href = ''; - if (baseTag && baseTag.getAttribute('href')) { - href = stripHash(window.location.href); - } - return href + '#' + encodePath(basename + createPath(location)); - } - - function push(path, state) { - warning( - state === undefined, - 'Hash history cannot push state; it is ignored' - ); - - const action = 'PUSH'; - const location = createLocation( - path, - undefined, - undefined, - history.location - ); - - transitionManager.confirmTransitionTo( - location, - action, - getUserConfirmation, - ok => { - if (!ok) return; - - const path = createPath(location); - const encodedPath = encodePath(basename + path); - const hashChanged = getHashPath() !== encodedPath; - - if (hashChanged) { - // We cannot tell if a hashchange was caused by a PUSH, so we'd - // rather setState here and ignore the hashchange. The caveat here - // is that other hash histories in the page will consider it a POP. - ignorePath = path; - pushHashPath(encodedPath); - - const prevIndex = allPaths.lastIndexOf(createPath(history.location)); - const nextPaths = allPaths.slice(0, prevIndex + 1); - - nextPaths.push(path); - allPaths = nextPaths; - - setState({ action, location }); - } else { - warning( - false, - 'Hash history cannot PUSH the same path; a new entry will not be added to the history stack' - ); - - setState(); - } - } - ); - } - - function replace(path, state) { - warning( - state === undefined, - 'Hash history cannot replace state; it is ignored' - ); - - const action = 'REPLACE'; - const location = createLocation( - path, - undefined, - undefined, - history.location - ); - - transitionManager.confirmTransitionTo( - location, - action, - getUserConfirmation, - ok => { - if (!ok) return; - - const path = createPath(location); - const encodedPath = encodePath(basename + path); - const hashChanged = getHashPath() !== encodedPath; - - if (hashChanged) { - // We cannot tell if a hashchange was caused by a REPLACE, so we'd - // rather setState here and ignore the hashchange. The caveat here - // is that other hash histories in the page will consider it a POP. - ignorePath = path; - replaceHashPath(encodedPath); - } - - const prevIndex = allPaths.indexOf(createPath(history.location)); - - if (prevIndex !== -1) allPaths[prevIndex] = path; - - setState({ action, location }); - } - ); - } - - function go(n) { - warning( - canGoWithoutReload, - 'Hash history go(n) causes a full page reload in this browser' - ); - - globalHistory.go(n); - } - - function goBack() { - go(-1); - } - - function goForward() { - go(1); - } - - let listenerCount = 0; - - function checkDOMListeners(delta) { - listenerCount += delta; - - if (listenerCount === 1 && delta === 1) { - window.addEventListener(HashChangeEvent, handleHashChange); - } else if (listenerCount === 0) { - window.removeEventListener(HashChangeEvent, handleHashChange); - } - } - - let isBlocked = false; - - function block(prompt = false) { - const unblock = transitionManager.setPrompt(prompt); - - if (!isBlocked) { - checkDOMListeners(1); - isBlocked = true; - } - - return () => { - if (isBlocked) { - isBlocked = false; - checkDOMListeners(-1); - } - - return unblock(); - }; - } - - function listen(listener) { - const unlisten = transitionManager.appendListener(listener); - checkDOMListeners(1); - - return () => { - checkDOMListeners(-1); - unlisten(); - }; - } - - const history = { - length: globalHistory.length, - action: 'POP', - location: initialLocation, - createHref, - push, - replace, - go, - goBack, - goForward, - block, - listen - }; - - return history; -} - -export default createHashHistory; diff --git a/modules/createMemoryHistory.js b/modules/createMemoryHistory.js deleted file mode 100644 index 8b541ea1c..000000000 --- a/modules/createMemoryHistory.js +++ /dev/null @@ -1,186 +0,0 @@ -import { createPath } from './PathUtils.js'; -import { createLocation } from './LocationUtils.js'; -import createTransitionManager from './createTransitionManager.js'; -import warning from './warning.js'; - -function clamp(n, lowerBound, upperBound) { - return Math.min(Math.max(n, lowerBound), upperBound); -} - -/** - * Creates a history object that stores locations in memory. - */ -function createMemoryHistory(props = {}) { - const { - getUserConfirmation, - initialEntries = ['/'], - initialIndex = 0, - keyLength = 6 - } = props; - - const transitionManager = createTransitionManager(); - - function setState(nextState) { - Object.assign(history, nextState); - history.length = history.entries.length; - transitionManager.notifyListeners(history.location, history.action); - } - - function createKey() { - return Math.random() - .toString(36) - .substr(2, keyLength); - } - - const index = clamp(initialIndex, 0, initialEntries.length - 1); - const entries = initialEntries.map(entry => - typeof entry === 'string' - ? createLocation(entry, undefined, createKey()) - : createLocation(entry, undefined, entry.key || createKey()) - ); - - // Public interface - - const createHref = createPath; - - function push(path, state) { - warning( - !( - typeof path === 'object' && - path.state !== undefined && - state !== undefined - ), - 'You should avoid providing a 2nd state argument to push when the 1st ' + - 'argument is a location-like object that already has state; it is ignored' - ); - - const action = 'PUSH'; - const location = createLocation(path, state, createKey(), history.location); - - transitionManager.confirmTransitionTo( - location, - action, - getUserConfirmation, - ok => { - if (!ok) return; - - const prevIndex = history.index; - const nextIndex = prevIndex + 1; - - const nextEntries = history.entries.slice(0); - if (nextEntries.length > nextIndex) { - nextEntries.splice( - nextIndex, - nextEntries.length - nextIndex, - location - ); - } else { - nextEntries.push(location); - } - - setState({ - action, - location, - index: nextIndex, - entries: nextEntries - }); - } - ); - } - - function replace(path, state) { - warning( - !( - typeof path === 'object' && - path.state !== undefined && - state !== undefined - ), - 'You should avoid providing a 2nd state argument to replace when the 1st ' + - 'argument is a location-like object that already has state; it is ignored' - ); - - const action = 'REPLACE'; - const location = createLocation(path, state, createKey(), history.location); - - transitionManager.confirmTransitionTo( - location, - action, - getUserConfirmation, - ok => { - if (!ok) return; - - history.entries[history.index] = location; - - setState({ action, location }); - } - ); - } - - function go(n) { - const nextIndex = clamp(history.index + n, 0, history.entries.length - 1); - - const action = 'POP'; - const location = history.entries[nextIndex]; - - transitionManager.confirmTransitionTo( - location, - action, - getUserConfirmation, - ok => { - if (ok) { - setState({ - action, - location, - index: nextIndex - }); - } else { - // Mimic the behavior of DOM histories by - // causing a render after a cancelled POP. - setState(); - } - } - ); - } - - function goBack() { - go(-1); - } - - function goForward() { - go(1); - } - - function canGo(n) { - const nextIndex = history.index + n; - return nextIndex >= 0 && nextIndex < history.entries.length; - } - - function block(prompt = false) { - return transitionManager.setPrompt(prompt); - } - - function listen(listener) { - return transitionManager.appendListener(listener); - } - - const history = { - length: entries.length, - action: 'POP', - location: entries[index], - index, - entries, - createHref, - push, - replace, - go, - goBack, - goForward, - canGo, - block, - listen - }; - - return history; -} - -export default createMemoryHistory; diff --git a/modules/createTransitionManager.js b/modules/createTransitionManager.js deleted file mode 100644 index 43732508c..000000000 --- a/modules/createTransitionManager.js +++ /dev/null @@ -1,78 +0,0 @@ -import warning from './warning.js'; - -function createTransitionManager() { - let prompt = null; - - function setPrompt(nextPrompt) { - warning(prompt == null, 'A history supports only one prompt at a time'); - - prompt = nextPrompt; - - return () => { - if (prompt === nextPrompt) prompt = null; - }; - } - - function confirmTransitionTo( - location, - action, - getUserConfirmation, - callback - ) { - // TODO: If another transition starts while we're still confirming - // the previous one, we may end up in a weird state. Figure out the - // best way to handle this. - if (prompt != null) { - const result = - typeof prompt === 'function' ? prompt(location, action) : prompt; - - if (typeof result === 'string') { - if (typeof getUserConfirmation === 'function') { - getUserConfirmation(result, callback); - } else { - warning( - false, - 'A history needs a getUserConfirmation function in order to use a prompt message' - ); - - callback(true); - } - } else { - // Return false from a transition hook to cancel the transition. - callback(result !== false); - } - } else { - callback(true); - } - } - - let listeners = []; - - function appendListener(fn) { - let isActive = true; - - function listener(...args) { - if (isActive) fn(...args); - } - - listeners.push(listener); - - return () => { - isActive = false; - listeners = listeners.filter(item => item !== listener); - }; - } - - function notifyListeners(...args) { - listeners.forEach(listener => listener(...args)); - } - - return { - setPrompt, - confirmTransitionTo, - appendListener, - notifyListeners - }; -} - -export default createTransitionManager; diff --git a/modules/index.js b/modules/index.js index b02317ddb..9422193e7 100644 --- a/modules/index.js +++ b/modules/index.js @@ -1,5 +1,363 @@ -export { default as createBrowserHistory } from './createBrowserHistory'; -export { default as createHashHistory } from './createHashHistory'; -export { default as createMemoryHistory } from './createMemoryHistory'; -export { createLocation, locationsAreEqual } from './LocationUtils'; -export { parsePath, createPath } from './PathUtils'; +const PopAction = 'POP'; +const PushAction = 'PUSH'; +const ReplaceAction = 'REPLACE'; + +const PopStateEvent = 'popstate'; +const HashChangeEvent = 'hashchange'; + +// There's some duplication in this code, but only one create* method +// should ever be used in a given web page, so it's best to just inline +// everything for minification. + +export const createMemoryHistory = ({ + initialEntries = ['/'], + initialIndex = 0 +} = {}) => { + let createLocation = ({ + pathname = '/', + search = '', + hash = '', + state = null, + // Auto-assign keys to entries that don't already have them. + key = createKey() + }) => createReadOnlyObject({ pathname, search, hash, state, key }); + + let createNextLocation = (to, state) => + createLocation({ + ...(typeof to === 'string' ? parsePath(to) : to), + state, + key: createKey() + }); + + let handleAction = (nextIndex, nextAction, nextLocation) => { + if (__DEV__) { + if (nextLocation && nextLocation.pathname.charAt(0) !== '/') { + let arg = createPath(nextLocation); + let fnCall = `${nextAction.toLowerCase()}("${arg}")`; + throw new Error( + `Relative pathnames are not supported in createMemoryHistory().${fnCall}` + ); + } + } + + if (blockers.length) { + blockers.call({ action: nextAction, location: nextLocation }); + } else { + index = nextIndex; + if (nextAction === PushAction) { + entries.splice(index, entries.length, nextLocation); + } else if (nextAction === ReplaceAction) { + entries[index] = nextLocation; + } + action = nextAction; + location = nextLocation || entries[index]; + listeners.call({ action, location }); + } + }; + + let entries = initialEntries.map(entry => + createLocation(typeof entry === 'string' ? parsePath(entry) : entry) + ); + let index = clamp(initialIndex, 0, entries.length - 1); + + let action = PopAction; + let location = entries[index]; + let blockers = createEvents(); + let listeners = createEvents(); + + let history = { + createHref: createPath, + block: fn => blockers.push(fn), + listen: fn => listeners.push(fn), + push: (to, state) => + handleAction(index + 1, PushAction, createNextLocation(to, state)), + replace: (to, state) => + handleAction(index, ReplaceAction, createNextLocation(to, state)), + go: n => handleAction(clamp(index + n, 0, entries.length - 1), PopAction), + back: () => history.go(-1), + forward: () => history.go(1) + }; + + Object.defineProperty(history, 'action', { + enumerable: true, + get: () => action + }); + + Object.defineProperty(history, 'location', { + enumerable: true, + get: () => location + }); + + return history; +}; + +export const createBrowserHistory = ({ + window = document.defaultView +} = {}) => { + let getLocation = () => { + let { pathname, search, hash } = window.location; + let { state } = window.history; + return createReadOnlyObject({ + pathname, + search, + hash, + state: (state && state.user) || null, + key: (state && state.key) || 'default' + }); + }; + + let createNextLocation = (to, state) => + createReadOnlyObject({ + ...(typeof to === 'string' + ? parsePath(to) + : { pathname: '/', search: '', hash: '', ...to }), + state, + key: createKey() + }); + + // TODO: Support forceRefresh and do NOT notify listeners when used. + let handleAction = (nextAction, nextLocation) => { + if (blockers.length) { + blockers.call({ action: nextAction, location: nextLocation }); + + if (nextAction === PopAction) { + // TODO: revert the POP + } + } else { + let state = { user: nextLocation.state, key: nextLocation.key }; + let url = createPath(nextLocation); + + if (nextAction === PushAction) { + // try...catch because iOS limits us to 100 pushState calls :/ + try { + window.history.pushState(state, null, url); + } catch (error) { + // They are going to lose state here, but there is no real + // way to warn them about it since the page will refresh... + window.location.assign(url); + } + } else if (nextAction === ReplaceAction) { + window.history.replaceState(state, null, url); + } + + action = nextAction; + location = getLocation(); + listeners.call({ action, location }); + } + }; + + let action = PopAction; + let location = getLocation(); + let blockers = createEvents(); + let listeners = createEvents(); + + window.addEventListener(PopStateEvent, event => { + handleAction(PopAction, getLocation()); + }); + + let history = { + createHref: createPath, + block: fn => blockers.push(fn), + listen: fn => listeners.push(fn), + push: (to, state) => + handleAction(PushAction, createNextLocation(to, state)), + replace: (to, state) => + handleAction(ReplaceAction, createNextLocation(to, state)), + go: n => window.history.go(n), + back: () => history.go(-1), + forward: () => history.go(1) + }; + + Object.defineProperty(history, 'action', { + enumerable: true, + get: () => action + }); + + Object.defineProperty(history, 'location', { + enumerable: true, + get: () => location + }); + + return history; +}; + +export const createHashHistory = ({ window = document.defaultView } = {}) => { + let getLocation = () => { + let { pathname, search, hash } = parsePath(window.location.hash.substr(1)); + let { state } = window.history; + return createReadOnlyObject({ + pathname, + search, + hash, + state: (state && state.user) || null, + key: (state && state.key) || 'default' + }); + }; + + let createNextLocation = (to, state) => + createReadOnlyObject({ + ...(typeof to === 'string' + ? parsePath(to) + : { pathname: '/', search: '', hash: '', ...to }), + state, + key: createKey() + }); + + let action = PopAction; + let location = getLocation(); + let blockers = createEvents(); + let listeners = createEvents(); + + // TODO: Support forceRefresh and do NOT notify listeners when used. + let handleAction = (nextAction, nextLocation) => { + if (__DEV__) { + if (nextLocation.pathname.charAt(0) !== '/') { + let arg = createPath(nextLocation); + let fnCall = `${nextAction.toLowerCase()}("${arg}")`; + throw new Error( + `Relative pathnames are not supported in createHashHistory().${fnCall}` + ); + } + } + + if (blockers.length) { + blockers.call({ action: nextAction, location: nextLocation }); + + if (nextAction === PopAction) { + // TODO: revert the POP + } + } else { + let state = { user: nextLocation.state, key: nextLocation.key }; + // TODO: Handle relative paths? Or just warn about them? + // TODO: Support different "hash types"? + let url = '#' + createPath(nextLocation); + + if (nextAction === PushAction) { + // try...catch because iOS limits us to 100 pushState calls :/ + try { + window.history.pushState(state, null, url); + } catch (error) { + // They are going to lose state here, but there is no real + // way to warn them about it since the page will refresh... + window.location.assign(url); + } + } else if (nextAction === ReplaceAction) { + window.history.replaceState(state, null, url); + } + + action = nextAction; + location = getLocation(); + listeners.call({ action, location }); + } + }; + + window.addEventListener(PopStateEvent, event => { + handleAction(PopAction, getLocation()); + }); + + window.addEventListener(HashChangeEvent, event => { + let nextLocation = getLocation(); + + // Ignore extraneous hashchange events. + if (createPath(nextLocation) !== createPath(location)) { + handleAction(PopAction, getLocation()); + } + }); + + let history = { + createHref: location => { + let base = document.querySelector('base'); + let href = ''; + if (base && base.getAttribute('href')) { + href = stripHash(window.location.href); + } + return href + '#' + createPath(location); + }, + block: fn => blockers.push(fn), + listen: fn => listeners.push(fn), + push: (to, state) => + handleAction(PushAction, createNextLocation(to, state)), + replace: (to, state) => + handleAction(ReplaceAction, createNextLocation(to, state)), + go: n => window.history.go(n), + back: () => history.go(-1), + forward: () => history.go(1) + }; + + Object.defineProperty(history, 'action', { + enumerable: true, + get: () => action + }); + + Object.defineProperty(history, 'location', { + enumerable: true, + get: () => location + }); + + return history; +}; + +// Utils + +const createKey = () => + Math.random() + .toString(36) + .substr(2, 8); + +// TODO: Probably only do this in dev? +const createReadOnlyObject = props => + Object.keys(props).reduce( + (obj, key) => + Object.defineProperty(obj, key, { enumerable: true, value: props[key] }), + Object.create(null) + ); + +const stripHash = url => { + let hashIndex = url.indexOf('#'); + return hashIndex === -1 ? url : url.slice(0, hashIndex); +}; + +const createPath = ({ pathname = '/', search = '', hash = '' }) => + pathname + search + hash; + +const parsePath = path => { + let pathname = path || '/'; + let search = ''; + let hash = ''; + + let hashIndex = pathname.indexOf('#'); + if (hashIndex >= 0) { + hash = pathname.substr(hashIndex); + pathname = pathname.substr(0, hashIndex); + } + + let searchIndex = pathname.indexOf('?'); + if (searchIndex >= 0) { + search = pathname.substr(searchIndex); + pathname = pathname.substr(0, searchIndex); + } + + return { pathname, search, hash }; +}; + +const createEvents = () => { + let handlers = []; + + return { + get length() { + return handlers.length; + }, + push(fn) { + handlers.push(fn); + return () => { + handlers = handlers.filter(handler => handler !== fn); + }; + }, + call(...args) { + handlers.forEach(fn => fn && fn(...args)); + } + }; +}; + +const clamp = (n, lowerBound, upperBound) => + Math.min(Math.max(n, lowerBound), upperBound); diff --git a/modules/invariant.js b/modules/invariant.js deleted file mode 100644 index 98b4860e9..000000000 --- a/modules/invariant.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'tiny-invariant'; diff --git a/modules/warning.js b/modules/warning.js deleted file mode 100644 index 510d489cd..000000000 --- a/modules/warning.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'tiny-warning'; diff --git a/package.json b/package.json index 4d2df9107..92c9935ce 100644 --- a/package.json +++ b/package.json @@ -23,11 +23,7 @@ }, "dependencies": { "@babel/runtime": "^7.1.2", - "loose-envify": "^1.2.0", - "resolve-pathname": "^3.0.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0", - "value-equal": "^1.0.1" + "loose-envify": "^1.2.0" }, "devDependencies": { "@babel/core": "^7.1.2", diff --git a/yarn.lock b/yarn.lock index b8ab3882c..ab0f75a6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4990,11 +4990,6 @@ resolve-from@^1.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" integrity sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY= -resolve-pathname@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" - integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== - resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" @@ -5718,16 +5713,6 @@ timers-browserify@^2.0.4: dependencies: setimmediate "^1.0.4" -tiny-invariant@^1.0.2: - version "1.0.6" - resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.6.tgz#b3f9b38835e36a41c843a3b0907a5a7b3755de73" - integrity sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA== - -tiny-warning@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" - integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== - tmp@0.0.33, tmp@0.0.x: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -6009,11 +5994,6 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" -value-equal@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" - integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== - vm-browserify@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.0.tgz#bd76d6a23323e2ca8ffa12028dc04559c75f9019" From d51e9b7eb09188d14f3b96b221fb56ef0441f1ab Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 5 Nov 2019 15:23:19 -0800 Subject: [PATCH 004/133] Use ES6 ESLint env --- modules/.eslintrc | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/.eslintrc b/modules/.eslintrc index 340546cde..146cf5cae 100644 --- a/modules/.eslintrc +++ b/modules/.eslintrc @@ -1,6 +1,7 @@ { "env": { "browser": true, + "es6": true, "node": false }, "globals": { From 749680710d53663a288b835a1cf1f0ad92968e57 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 5 Nov 2019 15:25:21 -0800 Subject: [PATCH 005/133] Tweak test function format --- modules/__tests__/TestSequences/BackButtonTransitionHook.js | 4 ++-- modules/__tests__/TestSequences/BlockEverything.js | 4 ++-- modules/__tests__/TestSequences/BlockPopWithoutListening.js | 4 ++-- .../__tests__/TestSequences/EncodedReservedCharacters.js | 4 ++-- modules/__tests__/TestSequences/GoBack.js | 4 ++-- modules/__tests__/TestSequences/GoForward.js | 4 ++-- .../__tests__/TestSequences/InitialLocationDefaultKey.js | 4 ++-- modules/__tests__/TestSequences/InitialLocationHasKey.js | 4 ++-- modules/__tests__/TestSequences/Listen.js | 4 ++-- .../__tests__/TestSequences/LocationPathnameAlwaysSame.js | 4 ++-- modules/__tests__/TestSequences/PushEncodedLocation.js | 4 ++-- modules/__tests__/TestSequences/PushMissingPathname.js | 4 ++-- modules/__tests__/TestSequences/PushNewLocation.js | 4 ++-- modules/__tests__/TestSequences/PushRelativePathname.js | 4 ++-- .../__tests__/TestSequences/PushRelativePathnameError.js | 4 ++-- modules/__tests__/TestSequences/PushSamePath.js | 4 ++-- modules/__tests__/TestSequences/PushState.js | 4 ++-- modules/__tests__/TestSequences/PushUnicodeLocation.js | 4 ++-- modules/__tests__/TestSequences/ReplaceNewLocation.js | 4 ++-- modules/__tests__/TestSequences/ReplaceSamePath.js | 4 ++-- modules/__tests__/TestSequences/ReplaceState.js | 4 ++-- modules/__tests__/TestSequences/execSteps.js | 6 ++++-- 22 files changed, 46 insertions(+), 44 deletions(-) diff --git a/modules/__tests__/TestSequences/BackButtonTransitionHook.js b/modules/__tests__/TestSequences/BackButtonTransitionHook.js index d0544e21c..59343e17f 100644 --- a/modules/__tests__/TestSequences/BackButtonTransitionHook.js +++ b/modules/__tests__/TestSequences/BackButtonTransitionHook.js @@ -2,7 +2,7 @@ import expect from 'expect'; import execSteps from './execSteps.js'; -export default function(history, done) { +export default (history, done) => { let hookWasCalled = false; let unblock; @@ -39,4 +39,4 @@ export default function(history, done) { ]; execSteps(steps, history, done); -} +}; diff --git a/modules/__tests__/TestSequences/BlockEverything.js b/modules/__tests__/TestSequences/BlockEverything.js index ab8db6324..bc1bd45de 100644 --- a/modules/__tests__/TestSequences/BlockEverything.js +++ b/modules/__tests__/TestSequences/BlockEverything.js @@ -2,7 +2,7 @@ import expect from 'expect'; import execSteps from './execSteps.js'; -export default function(history, done) { +export default (history, done) => { let steps = [ ({ location }) => { expect(location).toMatchObject({ @@ -22,4 +22,4 @@ export default function(history, done) { ]; execSteps(steps, history, done); -} +}; diff --git a/modules/__tests__/TestSequences/BlockPopWithoutListening.js b/modules/__tests__/TestSequences/BlockPopWithoutListening.js index f98887edc..3925870e8 100644 --- a/modules/__tests__/TestSequences/BlockPopWithoutListening.js +++ b/modules/__tests__/TestSequences/BlockPopWithoutListening.js @@ -1,6 +1,6 @@ import expect from 'expect'; -export default function(history, done) { +export default (history, done) => { expect(history.location).toMatchObject({ pathname: '/' }); @@ -30,4 +30,4 @@ export default function(history, done) { done(); }, 100); }, 10); -} +}; diff --git a/modules/__tests__/TestSequences/EncodedReservedCharacters.js b/modules/__tests__/TestSequences/EncodedReservedCharacters.js index b0addd0df..bc4191810 100644 --- a/modules/__tests__/TestSequences/EncodedReservedCharacters.js +++ b/modules/__tests__/TestSequences/EncodedReservedCharacters.js @@ -2,7 +2,7 @@ import expect from 'expect'; import execSteps from './execSteps.js'; -export default function(history, done) { +export default (history, done) => { let steps = [ () => { // encoded string @@ -35,4 +35,4 @@ export default function(history, done) { ]; execSteps(steps, history, done); -} +}; diff --git a/modules/__tests__/TestSequences/GoBack.js b/modules/__tests__/TestSequences/GoBack.js index 2f582e06f..9ff55501f 100644 --- a/modules/__tests__/TestSequences/GoBack.js +++ b/modules/__tests__/TestSequences/GoBack.js @@ -2,7 +2,7 @@ import expect from 'expect'; import execSteps from './execSteps.js'; -export default function(history, done) { +export default (history, done) => { let steps = [ ({ location }) => { expect(location).toMatchObject({ @@ -28,4 +28,4 @@ export default function(history, done) { ]; execSteps(steps, history, done); -} +}; diff --git a/modules/__tests__/TestSequences/GoForward.js b/modules/__tests__/TestSequences/GoForward.js index 7a5a6c416..94bdf42c4 100644 --- a/modules/__tests__/TestSequences/GoForward.js +++ b/modules/__tests__/TestSequences/GoForward.js @@ -2,7 +2,7 @@ import expect from 'expect'; import execSteps from './execSteps.js'; -export default function(history, done) { +export default (history, done) => { let steps = [ ({ location }) => { expect(location).toMatchObject({ @@ -36,4 +36,4 @@ export default function(history, done) { ]; execSteps(steps, history, done); -} +}; diff --git a/modules/__tests__/TestSequences/InitialLocationDefaultKey.js b/modules/__tests__/TestSequences/InitialLocationDefaultKey.js index eb13fee25..aefa11f54 100644 --- a/modules/__tests__/TestSequences/InitialLocationDefaultKey.js +++ b/modules/__tests__/TestSequences/InitialLocationDefaultKey.js @@ -2,7 +2,7 @@ import expect from 'expect'; import execSteps from './execSteps.js'; -export default function(history, done) { +export default (history, done) => { let steps = [ ({ location }) => { expect(location.key).toBe('default'); @@ -10,4 +10,4 @@ export default function(history, done) { ]; execSteps(steps, history, done); -} +}; diff --git a/modules/__tests__/TestSequences/InitialLocationHasKey.js b/modules/__tests__/TestSequences/InitialLocationHasKey.js index f506fb5b2..c06502a7a 100644 --- a/modules/__tests__/TestSequences/InitialLocationHasKey.js +++ b/modules/__tests__/TestSequences/InitialLocationHasKey.js @@ -2,7 +2,7 @@ import expect from 'expect'; import execSteps from './execSteps.js'; -export default function(history, done) { +export default (history, done) => { let steps = [ ({ location }) => { expect(location.key).toBeTruthy(); @@ -10,4 +10,4 @@ export default function(history, done) { ]; execSteps(steps, history, done); -} +}; diff --git a/modules/__tests__/TestSequences/Listen.js b/modules/__tests__/TestSequences/Listen.js index ddc6c4998..b27502fbc 100644 --- a/modules/__tests__/TestSequences/Listen.js +++ b/modules/__tests__/TestSequences/Listen.js @@ -1,7 +1,7 @@ import expect from 'expect'; import mock from 'jest-mock'; -export default function(history, done) { +export default (history, done) => { let spy = mock.fn(); let unlisten = history.listen(spy); @@ -9,4 +9,4 @@ export default function(history, done) { unlisten(); done(); -} +}; diff --git a/modules/__tests__/TestSequences/LocationPathnameAlwaysSame.js b/modules/__tests__/TestSequences/LocationPathnameAlwaysSame.js index 5a6158fef..24aef37ad 100644 --- a/modules/__tests__/TestSequences/LocationPathnameAlwaysSame.js +++ b/modules/__tests__/TestSequences/LocationPathnameAlwaysSame.js @@ -2,7 +2,7 @@ import expect from 'expect'; import execSteps from './execSteps.js'; -export default function(history, done) { +export default (history, done) => { let steps = [ () => { // encoded string @@ -41,4 +41,4 @@ export default function(history, done) { ]; execSteps(steps, history, done); -} +}; diff --git a/modules/__tests__/TestSequences/PushEncodedLocation.js b/modules/__tests__/TestSequences/PushEncodedLocation.js index 60a369303..b55137e34 100644 --- a/modules/__tests__/TestSequences/PushEncodedLocation.js +++ b/modules/__tests__/TestSequences/PushEncodedLocation.js @@ -2,7 +2,7 @@ import expect from 'expect'; import execSteps from './execSteps.js'; -export default function(history, done) { +export default (history, done) => { let steps = [ ({ location }) => { expect(location).toMatchObject({ @@ -26,4 +26,4 @@ export default function(history, done) { ]; execSteps(steps, history, done); -} +}; diff --git a/modules/__tests__/TestSequences/PushMissingPathname.js b/modules/__tests__/TestSequences/PushMissingPathname.js index 1f3499afa..dd7f8a8e1 100644 --- a/modules/__tests__/TestSequences/PushMissingPathname.js +++ b/modules/__tests__/TestSequences/PushMissingPathname.js @@ -2,7 +2,7 @@ import expect from 'expect'; import execSteps from './execSteps.js'; -export default function(history, done) { +export default (history, done) => { let steps = [ ({ location }) => { expect(location).toMatchObject({ @@ -32,4 +32,4 @@ export default function(history, done) { ]; execSteps(steps, history, done); -} +}; diff --git a/modules/__tests__/TestSequences/PushNewLocation.js b/modules/__tests__/TestSequences/PushNewLocation.js index 156bef476..21e8e86f1 100644 --- a/modules/__tests__/TestSequences/PushNewLocation.js +++ b/modules/__tests__/TestSequences/PushNewLocation.js @@ -2,7 +2,7 @@ import expect from 'expect'; import execSteps from './execSteps.js'; -export default function(history, done) { +export default (history, done) => { let steps = [ ({ location }) => { expect(location).toMatchObject({ @@ -22,4 +22,4 @@ export default function(history, done) { ]; execSteps(steps, history, done); -} +}; diff --git a/modules/__tests__/TestSequences/PushRelativePathname.js b/modules/__tests__/TestSequences/PushRelativePathname.js index 75850e724..8e14581d5 100644 --- a/modules/__tests__/TestSequences/PushRelativePathname.js +++ b/modules/__tests__/TestSequences/PushRelativePathname.js @@ -2,7 +2,7 @@ import expect from 'expect'; import execSteps from './execSteps.js'; -export default function(history, done) { +export default (history, done) => { let steps = [ ({ location }) => { expect(location).toMatchObject({ @@ -32,4 +32,4 @@ export default function(history, done) { ]; execSteps(steps, history, done); -} +}; diff --git a/modules/__tests__/TestSequences/PushRelativePathnameError.js b/modules/__tests__/TestSequences/PushRelativePathnameError.js index 7c8a0759d..3f50a2a18 100644 --- a/modules/__tests__/TestSequences/PushRelativePathnameError.js +++ b/modules/__tests__/TestSequences/PushRelativePathnameError.js @@ -2,7 +2,7 @@ import expect from 'expect'; import execSteps from './execSteps.js'; -export default function(history, done) { +export default (history, done) => { let steps = [ ({ location }) => { expect(location).toMatchObject({ @@ -28,4 +28,4 @@ export default function(history, done) { ]; execSteps(steps, history, done); -} +}; diff --git a/modules/__tests__/TestSequences/PushSamePath.js b/modules/__tests__/TestSequences/PushSamePath.js index 765ae661b..b3a12628e 100644 --- a/modules/__tests__/TestSequences/PushSamePath.js +++ b/modules/__tests__/TestSequences/PushSamePath.js @@ -2,7 +2,7 @@ import expect from 'expect'; import execSteps from './execSteps.js'; -export default function(history, done) { +export default (history, done) => { let steps = [ ({ location }) => { expect(location).toMatchObject({ @@ -36,4 +36,4 @@ export default function(history, done) { ]; execSteps(steps, history, done); -} +}; diff --git a/modules/__tests__/TestSequences/PushState.js b/modules/__tests__/TestSequences/PushState.js index 0d111e61b..70f02bb5b 100644 --- a/modules/__tests__/TestSequences/PushState.js +++ b/modules/__tests__/TestSequences/PushState.js @@ -2,7 +2,7 @@ import expect from 'expect'; import execSteps from './execSteps.js'; -export default function(history, done) { +export default (history, done) => { let steps = [ ({ location }) => { expect(location).toMatchObject({ @@ -23,4 +23,4 @@ export default function(history, done) { ]; execSteps(steps, history, done); -} +}; diff --git a/modules/__tests__/TestSequences/PushUnicodeLocation.js b/modules/__tests__/TestSequences/PushUnicodeLocation.js index 8401c2c87..b9b3c0913 100644 --- a/modules/__tests__/TestSequences/PushUnicodeLocation.js +++ b/modules/__tests__/TestSequences/PushUnicodeLocation.js @@ -2,7 +2,7 @@ import expect from 'expect'; import execSteps from './execSteps.js'; -export default function(history, done) { +export default (history, done) => { let steps = [ ({ location }) => { expect(location).toMatchObject({ @@ -28,4 +28,4 @@ export default function(history, done) { ]; execSteps(steps, history, done); -} +}; diff --git a/modules/__tests__/TestSequences/ReplaceNewLocation.js b/modules/__tests__/TestSequences/ReplaceNewLocation.js index fcfb3c421..0c4f93fbd 100644 --- a/modules/__tests__/TestSequences/ReplaceNewLocation.js +++ b/modules/__tests__/TestSequences/ReplaceNewLocation.js @@ -2,7 +2,7 @@ import expect from 'expect'; import execSteps from './execSteps.js'; -export default function(history, done) { +export default (history, done) => { let steps = [ ({ location }) => { expect(location).toMatchObject({ @@ -22,4 +22,4 @@ export default function(history, done) { ]; execSteps(steps, history, done); -} +}; diff --git a/modules/__tests__/TestSequences/ReplaceSamePath.js b/modules/__tests__/TestSequences/ReplaceSamePath.js index ffd292f09..df06a3400 100644 --- a/modules/__tests__/TestSequences/ReplaceSamePath.js +++ b/modules/__tests__/TestSequences/ReplaceSamePath.js @@ -2,7 +2,7 @@ import expect from 'expect'; import execSteps from './execSteps.js'; -export default function(history, done) { +export default (history, done) => { let prevLocation; let steps = [ @@ -34,4 +34,4 @@ export default function(history, done) { ]; execSteps(steps, history, done); -} +}; diff --git a/modules/__tests__/TestSequences/ReplaceState.js b/modules/__tests__/TestSequences/ReplaceState.js index 7e3e50a61..56f7ad178 100644 --- a/modules/__tests__/TestSequences/ReplaceState.js +++ b/modules/__tests__/TestSequences/ReplaceState.js @@ -2,7 +2,7 @@ import expect from 'expect'; import execSteps from './execSteps.js'; -export default function(history, done) { +export default (history, done) => { let steps = [ ({ location }) => { expect(location).toMatchObject({ @@ -23,4 +23,4 @@ export default function(history, done) { ]; execSteps(steps, history, done); -} +}; diff --git a/modules/__tests__/TestSequences/execSteps.js b/modules/__tests__/TestSequences/execSteps.js index 0e3c836a7..fb3241af7 100644 --- a/modules/__tests__/TestSequences/execSteps.js +++ b/modules/__tests__/TestSequences/execSteps.js @@ -1,4 +1,4 @@ -export default function execSteps(steps, history, done) { +const execSteps = (steps, history, done) => { let index = 0, unlisten, cleanedUp = false; @@ -35,4 +35,6 @@ export default function execSteps(steps, history, done) { } else { done(); } -} +}; + +export default execSteps; From 824ed2978f1c25e04673b015a3f77debeb8ac492 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 5 Nov 2019 17:30:07 -0800 Subject: [PATCH 006/133] Consolidate push/replace => navigate --- .size-snapshot.json | 18 ++-- .../TestSequences/BackButtonTransitionHook.js | 2 +- .../TestSequences/BlockEverything.js | 2 +- .../TestSequences/BlockPopWithoutListening.js | 2 +- .../EncodedReservedCharacters.js | 6 +- modules/__tests__/TestSequences/GoBack.js | 2 +- modules/__tests__/TestSequences/GoForward.js | 2 +- .../LocationPathnameAlwaysSame.js | 8 +- .../TestSequences/PushEncodedLocation.js | 2 +- .../TestSequences/PushMissingPathname.js | 4 +- .../TestSequences/PushNewLocation.js | 2 +- .../TestSequences/PushRelativePathname.js | 4 +- .../PushRelativePathnameError.js | 4 +- .../__tests__/TestSequences/PushSamePath.js | 4 +- modules/__tests__/TestSequences/PushState.js | 2 +- .../TestSequences/PushUnicodeLocation.js | 2 +- .../TestSequences/ReplaceNewLocation.js | 2 +- .../TestSequences/ReplaceSamePath.js | 4 +- .../__tests__/TestSequences/ReplaceState.js | 5 +- modules/index.js | 97 +++++++++---------- 20 files changed, 85 insertions(+), 89 deletions(-) diff --git a/.size-snapshot.json b/.size-snapshot.json index e02c3164f..ef1d954ff 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -1,8 +1,8 @@ { "esm/history.js": { - "bundled": 13433, - "minified": 5829, - "gzipped": 1706, + "bundled": 13550, + "minified": 5796, + "gzipped": 1690, "treeshaked": { "rollup": { "code": 43, @@ -14,13 +14,13 @@ } }, "umd/history.js": { - "bundled": 14957, - "minified": 5450, - "gzipped": 1760 + "bundled": 15060, + "minified": 5417, + "gzipped": 1750 }, "umd/history.min.js": { - "bundled": 14343, - "minified": 5137, - "gzipped": 1619 + "bundled": 14446, + "minified": 5104, + "gzipped": 1611 } } diff --git a/modules/__tests__/TestSequences/BackButtonTransitionHook.js b/modules/__tests__/TestSequences/BackButtonTransitionHook.js index 59343e17f..8f197bde1 100644 --- a/modules/__tests__/TestSequences/BackButtonTransitionHook.js +++ b/modules/__tests__/TestSequences/BackButtonTransitionHook.js @@ -12,7 +12,7 @@ export default (history, done) => { pathname: '/' }); - history.push('/home'); + history.navigate('/home'); }, ({ action, location }) => { expect(action).toBe('PUSH'); diff --git a/modules/__tests__/TestSequences/BlockEverything.js b/modules/__tests__/TestSequences/BlockEverything.js index bc1bd45de..083d30c86 100644 --- a/modules/__tests__/TestSequences/BlockEverything.js +++ b/modules/__tests__/TestSequences/BlockEverything.js @@ -11,7 +11,7 @@ export default (history, done) => { let unblock = history.block(); - history.push('/home'); + history.navigate('/home'); expect(history.location).toMatchObject({ pathname: '/' diff --git a/modules/__tests__/TestSequences/BlockPopWithoutListening.js b/modules/__tests__/TestSequences/BlockPopWithoutListening.js index 3925870e8..8b8203d93 100644 --- a/modules/__tests__/TestSequences/BlockPopWithoutListening.js +++ b/modules/__tests__/TestSequences/BlockPopWithoutListening.js @@ -5,7 +5,7 @@ export default (history, done) => { pathname: '/' }); - history.push('/home'); + history.navigate('/home'); let transitionHookWasCalled = false; let unblock = history.block(() => { diff --git a/modules/__tests__/TestSequences/EncodedReservedCharacters.js b/modules/__tests__/TestSequences/EncodedReservedCharacters.js index bc4191810..70cea3e52 100644 --- a/modules/__tests__/TestSequences/EncodedReservedCharacters.js +++ b/modules/__tests__/TestSequences/EncodedReservedCharacters.js @@ -7,7 +7,7 @@ export default (history, done) => { () => { // encoded string let pathname = '/view/%23abc'; - history.replace(pathname); + history.navigate(pathname, { replace: true }); }, ({ location }) => { expect(location).toMatchObject({ @@ -16,7 +16,7 @@ export default (history, done) => { // encoded object let pathname = '/view/%23abc'; - history.replace({ pathname }); + history.navigate({ pathname }, { replace: true }); }, ({ location }) => { expect(location).toMatchObject({ @@ -24,7 +24,7 @@ export default (history, done) => { }); // unencoded string let pathname = '/view/#abc'; - history.replace(pathname); + history.navigate(pathname, { replace: true }); }, ({ location }) => { expect(location).toMatchObject({ diff --git a/modules/__tests__/TestSequences/GoBack.js b/modules/__tests__/TestSequences/GoBack.js index 9ff55501f..11b8c02e4 100644 --- a/modules/__tests__/TestSequences/GoBack.js +++ b/modules/__tests__/TestSequences/GoBack.js @@ -9,7 +9,7 @@ export default (history, done) => { pathname: '/' }); - history.push('/home'); + history.navigate('/home'); }, ({ action, location }) => { expect(action).toEqual('PUSH'); diff --git a/modules/__tests__/TestSequences/GoForward.js b/modules/__tests__/TestSequences/GoForward.js index 94bdf42c4..17e4f2375 100644 --- a/modules/__tests__/TestSequences/GoForward.js +++ b/modules/__tests__/TestSequences/GoForward.js @@ -9,7 +9,7 @@ export default (history, done) => { pathname: '/' }); - history.push('/home'); + history.navigate('/home'); }, ({ action, location }) => { expect(action).toEqual('PUSH'); diff --git a/modules/__tests__/TestSequences/LocationPathnameAlwaysSame.js b/modules/__tests__/TestSequences/LocationPathnameAlwaysSame.js index 24aef37ad..adb9d5548 100644 --- a/modules/__tests__/TestSequences/LocationPathnameAlwaysSame.js +++ b/modules/__tests__/TestSequences/LocationPathnameAlwaysSame.js @@ -7,7 +7,7 @@ export default (history, done) => { () => { // encoded string let pathname = '/%E6%AD%B4%E5%8F%B2'; - history.replace(pathname); + history.navigate(pathname, { replace: true }); }, ({ location }) => { expect(location).toMatchObject({ @@ -15,7 +15,7 @@ export default (history, done) => { }); // encoded object let pathname = '/%E6%AD%B4%E5%8F%B2'; - history.replace({ pathname }); + history.navigate({ pathname }, { replace: true }); }, ({ location }) => { expect(location).toMatchObject({ @@ -23,7 +23,7 @@ export default (history, done) => { }); // unencoded string let pathname = '/歴史'; - history.replace(pathname); + history.navigate(pathname, { replace: true }); }, ({ location }) => { expect(location).toMatchObject({ @@ -31,7 +31,7 @@ export default (history, done) => { }); // unencoded object let pathname = '/歴史'; - history.replace({ pathname }); + history.navigate({ pathname }, { replace: true }); }, ({ location }) => { expect(location).toMatchObject({ diff --git a/modules/__tests__/TestSequences/PushEncodedLocation.js b/modules/__tests__/TestSequences/PushEncodedLocation.js index b55137e34..782d2b551 100644 --- a/modules/__tests__/TestSequences/PushEncodedLocation.js +++ b/modules/__tests__/TestSequences/PushEncodedLocation.js @@ -12,7 +12,7 @@ export default (history, done) => { let pathname = '/歴史'; let search = '?%E3%82%AD%E3%83%BC=%E5%80%A4'; let hash = '#%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5'; - history.push(pathname + search + hash); + history.navigate(pathname + search + hash); }, ({ action, location }) => { expect(action).toBe('PUSH'); diff --git a/modules/__tests__/TestSequences/PushMissingPathname.js b/modules/__tests__/TestSequences/PushMissingPathname.js index dd7f8a8e1..863f18896 100644 --- a/modules/__tests__/TestSequences/PushMissingPathname.js +++ b/modules/__tests__/TestSequences/PushMissingPathname.js @@ -9,7 +9,7 @@ export default (history, done) => { pathname: '/' }); - history.push('/home?the=query#the-hash'); + history.navigate('/home?the=query#the-hash'); }, ({ action, location }) => { expect(action).toBe('PUSH'); @@ -19,7 +19,7 @@ export default (history, done) => { hash: '#the-hash' }); - history.push('?another=query#another-hash'); + history.navigate('?another=query#another-hash'); }, ({ action, location }) => { expect(action).toBe('PUSH'); diff --git a/modules/__tests__/TestSequences/PushNewLocation.js b/modules/__tests__/TestSequences/PushNewLocation.js index 21e8e86f1..abe0697a5 100644 --- a/modules/__tests__/TestSequences/PushNewLocation.js +++ b/modules/__tests__/TestSequences/PushNewLocation.js @@ -9,7 +9,7 @@ export default (history, done) => { pathname: '/' }); - history.push('/home?the=query#the-hash'); + history.navigate('/home?the=query#the-hash'); }, ({ action, location }) => { expect(action).toBe('PUSH'); diff --git a/modules/__tests__/TestSequences/PushRelativePathname.js b/modules/__tests__/TestSequences/PushRelativePathname.js index 8e14581d5..cd0880571 100644 --- a/modules/__tests__/TestSequences/PushRelativePathname.js +++ b/modules/__tests__/TestSequences/PushRelativePathname.js @@ -9,7 +9,7 @@ export default (history, done) => { pathname: '/' }); - history.push('/the/path?the=query#the-hash'); + history.navigate('/the/path?the=query#the-hash'); }, ({ action, location }) => { expect(action).toBe('PUSH'); @@ -19,7 +19,7 @@ export default (history, done) => { hash: '#the-hash' }); - history.push('../other/path?another=query#another-hash'); + history.navigate('../other/path?another=query#another-hash'); }, ({ action, location }) => { expect(action).toBe('PUSH'); diff --git a/modules/__tests__/TestSequences/PushRelativePathnameError.js b/modules/__tests__/TestSequences/PushRelativePathnameError.js index 3f50a2a18..a5155ef86 100644 --- a/modules/__tests__/TestSequences/PushRelativePathnameError.js +++ b/modules/__tests__/TestSequences/PushRelativePathnameError.js @@ -9,7 +9,7 @@ export default (history, done) => { pathname: '/' }); - history.push('/the/path?the=query#the-hash'); + history.navigate('/the/path?the=query#the-hash'); }, ({ action, location }) => { expect(action).toBe('PUSH'); @@ -20,7 +20,7 @@ export default (history, done) => { }); try { - history.push('../other/path?another=query#another-hash'); + history.navigate('../other/path?another=query#another-hash'); } catch (error) { expect(error.message).toMatch(/relative pathnames are not supported/i); } diff --git a/modules/__tests__/TestSequences/PushSamePath.js b/modules/__tests__/TestSequences/PushSamePath.js index b3a12628e..cf1780faa 100644 --- a/modules/__tests__/TestSequences/PushSamePath.js +++ b/modules/__tests__/TestSequences/PushSamePath.js @@ -9,7 +9,7 @@ export default (history, done) => { pathname: '/' }); - history.push('/home'); + history.navigate('/home'); }, ({ action, location }) => { expect(action).toBe('PUSH'); @@ -17,7 +17,7 @@ export default (history, done) => { pathname: '/home' }); - history.push('/home'); + history.navigate('/home'); }, ({ action, location }) => { expect(action).toBe('PUSH'); diff --git a/modules/__tests__/TestSequences/PushState.js b/modules/__tests__/TestSequences/PushState.js index 70f02bb5b..1d1e720c8 100644 --- a/modules/__tests__/TestSequences/PushState.js +++ b/modules/__tests__/TestSequences/PushState.js @@ -9,7 +9,7 @@ export default (history, done) => { pathname: '/' }); - history.push('/home?the=query#the-hash', { the: 'state' }); + history.navigate('/home?the=query#the-hash', { state: { the: 'state' } }); }, ({ action, location }) => { expect(action).toBe('PUSH'); diff --git a/modules/__tests__/TestSequences/PushUnicodeLocation.js b/modules/__tests__/TestSequences/PushUnicodeLocation.js index b9b3c0913..2e92003c4 100644 --- a/modules/__tests__/TestSequences/PushUnicodeLocation.js +++ b/modules/__tests__/TestSequences/PushUnicodeLocation.js @@ -12,7 +12,7 @@ export default (history, done) => { let pathname = '/歴史'; let search = '?キー=値'; let hash = '#ハッシュ'; - history.push(pathname + search + hash); + history.navigate(pathname + search + hash); }, ({ action, location }) => { expect(action).toBe('PUSH'); diff --git a/modules/__tests__/TestSequences/ReplaceNewLocation.js b/modules/__tests__/TestSequences/ReplaceNewLocation.js index 0c4f93fbd..5a5d9d15f 100644 --- a/modules/__tests__/TestSequences/ReplaceNewLocation.js +++ b/modules/__tests__/TestSequences/ReplaceNewLocation.js @@ -9,7 +9,7 @@ export default (history, done) => { pathname: '/' }); - history.replace('/home?the=query#the-hash'); + history.navigate('/home?the=query#the-hash', { replace: true }); }, ({ action, location }) => { expect(action).toBe('REPLACE'); diff --git a/modules/__tests__/TestSequences/ReplaceSamePath.js b/modules/__tests__/TestSequences/ReplaceSamePath.js index df06a3400..06d4a4c42 100644 --- a/modules/__tests__/TestSequences/ReplaceSamePath.js +++ b/modules/__tests__/TestSequences/ReplaceSamePath.js @@ -11,7 +11,7 @@ export default (history, done) => { pathname: '/' }); - history.replace('/home'); + history.navigate('/home', { replace: true }); }, ({ action, location }) => { expect(action).toBe('REPLACE'); @@ -21,7 +21,7 @@ export default (history, done) => { prevLocation = location; - history.replace('/home'); + history.navigate('/home', { replace: true }); }, ({ action, location }) => { expect(action).toBe('REPLACE'); diff --git a/modules/__tests__/TestSequences/ReplaceState.js b/modules/__tests__/TestSequences/ReplaceState.js index 56f7ad178..2622e89e6 100644 --- a/modules/__tests__/TestSequences/ReplaceState.js +++ b/modules/__tests__/TestSequences/ReplaceState.js @@ -9,7 +9,10 @@ export default (history, done) => { pathname: '/' }); - history.replace('/home?the=query#the-hash', { the: 'state' }); + history.navigate('/home?the=query#the-hash', { + replace: true, + state: { the: 'state' } + }); }, ({ action, location }) => { expect(action).toBe('REPLACE'); diff --git a/modules/index.js b/modules/index.js index 9422193e7..13525777e 100644 --- a/modules/index.js +++ b/modules/index.js @@ -22,14 +22,7 @@ export const createMemoryHistory = ({ key = createKey() }) => createReadOnlyObject({ pathname, search, hash, state, key }); - let createNextLocation = (to, state) => - createLocation({ - ...(typeof to === 'string' ? parsePath(to) : to), - state, - key: createKey() - }); - - let handleAction = (nextIndex, nextAction, nextLocation) => { + let handleNavigation = (nextIndex, nextAction, nextLocation) => { if (__DEV__) { if (nextLocation && nextLocation.pathname.charAt(0) !== '/') { let arg = createPath(nextLocation); @@ -69,11 +62,18 @@ export const createMemoryHistory = ({ createHref: createPath, block: fn => blockers.push(fn), listen: fn => listeners.push(fn), - push: (to, state) => - handleAction(index + 1, PushAction, createNextLocation(to, state)), - replace: (to, state) => - handleAction(index, ReplaceAction, createNextLocation(to, state)), - go: n => handleAction(clamp(index + n, 0, entries.length - 1), PopAction), + navigate: (to, { replace = false, state = null } = {}) => + handleNavigation( + replace ? index : index + 1, + replace ? ReplaceAction : PushAction, + createLocation({ + ...(typeof to === 'string' ? parsePath(to) : to), + state, + key: createKey() + }) + ), + go: n => + handleNavigation(clamp(index + n, 0, entries.length - 1), PopAction), back: () => history.go(-1), forward: () => history.go(1) }; @@ -106,17 +106,8 @@ export const createBrowserHistory = ({ }); }; - let createNextLocation = (to, state) => - createReadOnlyObject({ - ...(typeof to === 'string' - ? parsePath(to) - : { pathname: '/', search: '', hash: '', ...to }), - state, - key: createKey() - }); - // TODO: Support forceRefresh and do NOT notify listeners when used. - let handleAction = (nextAction, nextLocation) => { + let handleNavigation = (nextAction, nextLocation) => { if (blockers.length) { blockers.call({ action: nextAction, location: nextLocation }); @@ -152,17 +143,24 @@ export const createBrowserHistory = ({ let listeners = createEvents(); window.addEventListener(PopStateEvent, event => { - handleAction(PopAction, getLocation()); + handleNavigation(PopAction, getLocation()); }); let history = { createHref: createPath, block: fn => blockers.push(fn), listen: fn => listeners.push(fn), - push: (to, state) => - handleAction(PushAction, createNextLocation(to, state)), - replace: (to, state) => - handleAction(ReplaceAction, createNextLocation(to, state)), + navigate: (to, { replace = false, state = null } = {}) => + handleNavigation( + replace ? ReplaceAction : PushAction, + createReadOnlyObject({ + ...(typeof to === 'string' + ? parsePath(to) + : { pathname: '/', search: '', hash: '', ...to }), + state, + key: createKey() + }) + ), go: n => window.history.go(n), back: () => history.go(-1), forward: () => history.go(1) @@ -194,22 +192,13 @@ export const createHashHistory = ({ window = document.defaultView } = {}) => { }); }; - let createNextLocation = (to, state) => - createReadOnlyObject({ - ...(typeof to === 'string' - ? parsePath(to) - : { pathname: '/', search: '', hash: '', ...to }), - state, - key: createKey() - }); - let action = PopAction; let location = getLocation(); let blockers = createEvents(); let listeners = createEvents(); // TODO: Support forceRefresh and do NOT notify listeners when used. - let handleAction = (nextAction, nextLocation) => { + let handleNavigation = (nextAction, nextLocation) => { if (__DEV__) { if (nextLocation.pathname.charAt(0) !== '/') { let arg = createPath(nextLocation); @@ -252,7 +241,7 @@ export const createHashHistory = ({ window = document.defaultView } = {}) => { }; window.addEventListener(PopStateEvent, event => { - handleAction(PopAction, getLocation()); + handleNavigation(PopAction, getLocation()); }); window.addEventListener(HashChangeEvent, event => { @@ -260,7 +249,7 @@ export const createHashHistory = ({ window = document.defaultView } = {}) => { // Ignore extraneous hashchange events. if (createPath(nextLocation) !== createPath(location)) { - handleAction(PopAction, getLocation()); + handleNavigation(PopAction, getLocation()); } }); @@ -275,10 +264,17 @@ export const createHashHistory = ({ window = document.defaultView } = {}) => { }, block: fn => blockers.push(fn), listen: fn => listeners.push(fn), - push: (to, state) => - handleAction(PushAction, createNextLocation(to, state)), - replace: (to, state) => - handleAction(ReplaceAction, createNextLocation(to, state)), + navigate: (to, { replace = false, state = null } = {}) => + handleNavigation( + replace ? ReplaceAction : PushAction, + createReadOnlyObject({ + ...(typeof to === 'string' + ? parsePath(to) + : { pathname: '/', search: '', hash: '', ...to }), + state, + key: createKey() + }) + ), go: n => window.history.go(n), back: () => history.go(-1), forward: () => history.go(1) @@ -347,15 +343,12 @@ const createEvents = () => { get length() { return handlers.length; }, - push(fn) { - handlers.push(fn); - return () => { + push: fn => + handlers.push(fn) && + (() => { handlers = handlers.filter(handler => handler !== fn); - }; - }, - call(...args) { - handlers.forEach(fn => fn && fn(...args)); - } + }), + call: arg => handlers.map(fn => fn && fn(arg)) }; }; From 3cc181f5be66c99b417f686c7541abdc9ed1ea90 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 5 Nov 2019 17:31:21 -0800 Subject: [PATCH 007/133] Add beforeunload fixture --- fixtures/unload.html | 129 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 fixtures/unload.html diff --git a/fixtures/unload.html b/fixtures/unload.html new file mode 100644 index 000000000..ebf60dbef --- /dev/null +++ b/fixtures/unload.html @@ -0,0 +1,129 @@ + + + +

Controls

+ +

+ +

+

+ back & forward:
+ + +

+

+ regular links:
+ home + one + two +

+

+ pushState & replaceState:
+ pushState(home) + pushState(one) + pushState(two) + replaceState(three) +

+ +

Test Scenarios

+ +

+

    +
  • Click block - Click back - You should be prompted to stay
  • +
  • Click block - Click home - You should be prompted to stay
  • +
  • Click block - Click one - You should be prompted to stay
  • +
  • Click block - Close the tab - You should be prompted to stay
  • +
  • Click pushState(one) - Click block - Click back - You should still be at /unload-one
  • +
  • Click pushState(one) - Click block - Click pushState(two) - You should still be at /unload-one
  • +
  • Click pushState(one) - Click pushState(two) - Click back - Click block - Click back - You should still be at /unload-one
  • +
  • Click pushState(one) - Click pushState(two) - Click back - Click block - Click forward - You should still be at /unload-one
  • +
  • Click pushState(one) - Click pushState(two) - Click replaceState(three) - Click block - Click back - You should still be at /unload-three
  • +
+

+ + + + From 4bf15b1929d263a654036dd723b8a9fd7277cd21 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 5 Nov 2019 22:49:32 -0800 Subject: [PATCH 008/133] Add location.index + fix blocking POP actions --- .gitignore | 4 +- .size-snapshot.json | 18 +- fixtures/unload-history.html | 97 ++++++++ fixtures/{unload.html => unload-plain.html} | 24 +- modules/index.js | 249 ++++++++++++++------ 5 files changed, 292 insertions(+), 100 deletions(-) create mode 100644 fixtures/unload-history.html rename fixtures/{unload.html => unload-plain.html} (84%) diff --git a/.gitignore b/.gitignore index 5ea170866..270814e64 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -/node_modules/ /esm/ +/fixtures/history.js /cjs/ +/node_modules/ /umd/ -/local.log diff --git a/.size-snapshot.json b/.size-snapshot.json index ef1d954ff..ffc5b5dbd 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -1,8 +1,8 @@ { "esm/history.js": { - "bundled": 13550, - "minified": 5796, - "gzipped": 1690, + "bundled": 16328, + "minified": 6304, + "gzipped": 1786, "treeshaked": { "rollup": { "code": 43, @@ -14,13 +14,13 @@ } }, "umd/history.js": { - "bundled": 15060, - "minified": 5417, - "gzipped": 1750 + "bundled": 17970, + "minified": 5813, + "gzipped": 1944 }, "umd/history.min.js": { - "bundled": 14446, - "minified": 5104, - "gzipped": 1611 + "bundled": 17356, + "minified": 5500, + "gzipped": 1813 } } diff --git a/fixtures/unload-history.html b/fixtures/unload-history.html new file mode 100644 index 000000000..11348a447 --- /dev/null +++ b/fixtures/unload-history.html @@ -0,0 +1,97 @@ + + + +

Controls

+ +

+ +

+

+ back & forward:
+ + +

+

+ regular links:
+ home + one + two +

+

+ pushState & replaceState:
+ pushState(home) + pushState(one) + pushState(two) + replaceState(three) +

+ +

Test Scenarios

+ +

+

    +
  • Click block - Click back - You should be prompted to stay
  • +
  • Click block - Click home - You should be prompted to stay
  • +
  • Click block - Click one - You should be prompted to stay
  • +
  • Click block - Close the tab - You should be prompted to stay
  • +
  • Click pushState(one) - Click block - Click back - You should still be at /unload-history-one
  • +
  • Click pushState(one) - Click block - Click pushState(two) - You should still be at /unload-history-one
  • +
  • Click pushState(one) - Click pushState(two) - Click back - Click block - Click back - You should still be at /unload-history-one
  • +
  • Click pushState(one) - Click pushState(two) - Click back - Click block - Click forward - You should still be at /unload-history-one
  • +
  • Click pushState(one) - Click pushState(two) - Click replaceState(three) - Click block - Click back - You should still be at /unload-history-three
  • +
+

+ + + + + + diff --git a/fixtures/unload.html b/fixtures/unload-plain.html similarity index 84% rename from fixtures/unload.html rename to fixtures/unload-plain.html index ebf60dbef..0314cc391 100644 --- a/fixtures/unload.html +++ b/fixtures/unload-plain.html @@ -13,16 +13,16 @@

Controls

regular links:
- home - one - two + home + one + two

pushState & replaceState:
- pushState(home) - pushState(one) - pushState(two) - replaceState(three) + pushState(home) + pushState(one) + pushState(two) + replaceState(three)

Test Scenarios

@@ -33,11 +33,11 @@

Test Scenarios

  • Click block - Click home - You should be prompted to stay
  • Click block - Click one - You should be prompted to stay
  • Click block - Close the tab - You should be prompted to stay
  • -
  • Click pushState(one) - Click block - Click back - You should still be at /unload-one
  • -
  • Click pushState(one) - Click block - Click pushState(two) - You should still be at /unload-one
  • -
  • Click pushState(one) - Click pushState(two) - Click back - Click block - Click back - You should still be at /unload-one
  • -
  • Click pushState(one) - Click pushState(two) - Click back - Click block - Click forward - You should still be at /unload-one
  • -
  • Click pushState(one) - Click pushState(two) - Click replaceState(three) - Click block - Click back - You should still be at /unload-three
  • +
  • Click pushState(one) - Click block - Click back - You should still be at /unload-plain-one
  • +
  • Click pushState(one) - Click block - Click pushState(two) - You should still be at /unload-plain-one
  • +
  • Click pushState(one) - Click pushState(two) - Click back - Click block - Click back - You should still be at /unload-plain-one
  • +
  • Click pushState(one) - Click pushState(two) - Click back - Click block - Click forward - You should still be at /unload-plain-one
  • +
  • Click pushState(one) - Click pushState(two) - Click replaceState(three) - Click block - Click back - You should still be at /unload-plain-three
  • diff --git a/modules/index.js b/modules/index.js index 13525777e..15ea9007d 100644 --- a/modules/index.js +++ b/modules/index.js @@ -2,6 +2,7 @@ const PopAction = 'POP'; const PushAction = 'PUSH'; const ReplaceAction = 'REPLACE'; +const BeforeUnloadEvent = 'beforeunload'; const PopStateEvent = 'popstate'; const HashChangeEvent = 'hashchange'; @@ -9,6 +10,11 @@ const HashChangeEvent = 'hashchange'; // should ever be used in a given web page, so it's best to just inline // everything for minification. +/** + * Memory history stores the current location in memory. It is designed + * for use in stateful non-browser environments like headless tests (in + * node.js) and React Native. + */ export const createMemoryHistory = ({ initialEntries = ['/'], initialIndex = 0 @@ -19,10 +25,11 @@ export const createMemoryHistory = ({ hash = '', state = null, // Auto-assign keys to entries that don't already have them. - key = createKey() - }) => createReadOnlyObject({ pathname, search, hash, state, key }); + key = createKey(), + index + }) => createReadOnlyObject({ pathname, search, hash, state, key, index }); - let handleNavigation = (nextIndex, nextAction, nextLocation) => { + let handleNavigation = (nextAction, nextLocation) => { if (__DEV__) { if (nextLocation && nextLocation.pathname.charAt(0) !== '/') { let arg = createPath(nextLocation); @@ -36,119 +43,169 @@ export const createMemoryHistory = ({ if (blockers.length) { blockers.call({ action: nextAction, location: nextLocation }); } else { - index = nextIndex; if (nextAction === PushAction) { - entries.splice(index, entries.length, nextLocation); + entries.splice(nextLocation.index, entries.length, nextLocation); } else if (nextAction === ReplaceAction) { - entries[index] = nextLocation; + entries[nextLocation.index] = nextLocation; } action = nextAction; - location = nextLocation || entries[index]; + location = nextLocation; listeners.call({ action, location }); } }; - let entries = initialEntries.map(entry => - createLocation(typeof entry === 'string' ? parsePath(entry) : entry) + let entries = initialEntries.map((entry, index) => + createLocation({ + ...(typeof entry === 'string' ? parsePath(entry) : entry), + index + }) ); - let index = clamp(initialIndex, 0, entries.length - 1); let action = PopAction; - let location = entries[index]; + let location = entries[clamp(initialIndex, 0, entries.length - 1)]; let blockers = createEvents(); let listeners = createEvents(); let history = { + get action() { + return action; + }, + get location() { + return location; + }, createHref: createPath, block: fn => blockers.push(fn), listen: fn => listeners.push(fn), navigate: (to, { replace = false, state = null } = {}) => handleNavigation( - replace ? index : index + 1, replace ? ReplaceAction : PushAction, createLocation({ ...(typeof to === 'string' ? parsePath(to) : to), state, - key: createKey() + key: createKey(), + index: location.index + (replace ? 0 : 1) }) ), go: n => - handleNavigation(clamp(index + n, 0, entries.length - 1), PopAction), + handleNavigation( + PopAction, + entries[clamp(location.index + n, 0, entries.length - 1)] + ), back: () => history.go(-1), forward: () => history.go(1) }; - Object.defineProperty(history, 'action', { - enumerable: true, - get: () => action - }); - - Object.defineProperty(history, 'location', { - enumerable: true, - get: () => location - }); - return history; }; +/** + * Browser history stores the location in regular URLs. This is the + * standard for most web apps, but it requires some configuration on + * the server to ensure you serve the same app at multiple URLs. + */ export const createBrowserHistory = ({ window = document.defaultView } = {}) => { + let globalHistory = window.history; + let getLocation = () => { let { pathname, search, hash } = window.location; - let { state } = window.history; + let state = globalHistory.state || {}; return createReadOnlyObject({ pathname, search, hash, - state: (state && state.user) || null, - key: (state && state.key) || 'default' + state: state.usr || null, + key: state.key || 'default', + index: state.idx }); }; - // TODO: Support forceRefresh and do NOT notify listeners when used. + let ignoreNextPop = false; + + // TODO: Add reload arg let handleNavigation = (nextAction, nextLocation) => { + if (nextAction === PopAction && ignoreNextPop) { + ignoreNextPop = false; + return; + } + if (blockers.length) { blockers.call({ action: nextAction, location: nextLocation }); - if (nextAction === PopAction) { - // TODO: revert the POP + if (nextAction === PopAction && nextLocation.index != null) { + // Revert the POP + let n = location.index - nextLocation.index; + if (n) { + ignoreNextPop = true; + globalHistory.go(n); + } } } else { - let state = { user: nextLocation.state, key: nextLocation.key }; + let state = { + usr: nextLocation.state, + key: nextLocation.key, + idx: nextLocation.index + }; let url = createPath(nextLocation); if (nextAction === PushAction) { // try...catch because iOS limits us to 100 pushState calls :/ try { - window.history.pushState(state, null, url); + globalHistory.pushState(state, null, url); } catch (error) { // They are going to lose state here, but there is no real // way to warn them about it since the page will refresh... window.location.assign(url); } } else if (nextAction === ReplaceAction) { - window.history.replaceState(state, null, url); + globalHistory.replaceState(state, null, url); } action = nextAction; + // Get the location fresh so we can support relative paths. location = getLocation(); listeners.call({ action, location }); } }; - let action = PopAction; - let location = getLocation(); - let blockers = createEvents(); - let listeners = createEvents(); + let toggleBeforeUnloadBlocker = on => + window[`${on ? 'add' : 'remove'}EventListener`]( + BeforeUnloadEvent, + preventUnload + ); + + // Initialize the index for this document. + globalHistory.replaceState({ ...globalHistory.state, idx: 0 }, null); window.addEventListener(PopStateEvent, event => { handleNavigation(PopAction, getLocation()); }); + let action = PopAction; + let location = getLocation(); + let blockers = createEvents(); + let listeners = createEvents(); + let history = { + get action() { + return action; + }, + get location() { + return location; + }, createHref: createPath, - block: fn => blockers.push(fn), + block: fn => { + let unblock = blockers.push(fn); + if (blockers.length === 1) toggleBeforeUnloadBlocker(1); + return () => { + unblock(); + // Remove the beforeunload listener so the document may be + // salvageable in the pagehide event. + // See https://html.spec.whatwg.org/#unloading-documents + if (!blockers.length) toggleBeforeUnloadBlocker(0); + }; + }, listen: fn => listeners.push(fn), navigate: (to, { replace = false, state = null } = {}) => handleNavigation( @@ -158,46 +215,43 @@ export const createBrowserHistory = ({ ? parsePath(to) : { pathname: '/', search: '', hash: '', ...to }), state, - key: createKey() + key: createKey(), + index: location.index + (replace ? 0 : 1) }) ), - go: n => window.history.go(n), + go: n => globalHistory.go(n), back: () => history.go(-1), forward: () => history.go(1) }; - Object.defineProperty(history, 'action', { - enumerable: true, - get: () => action - }); - - Object.defineProperty(history, 'location', { - enumerable: true, - get: () => location - }); - return history; }; +/** + * Hash history stores the location in window.location.hash. This makes + * it ideal for situations where you don't want to send the location to + * the server for some reason, either because you do cannot configure it + * or the URL space is reserved for something else. + */ export const createHashHistory = ({ window = document.defaultView } = {}) => { + let globalHistory = window.history; + let getLocation = () => { let { pathname, search, hash } = parsePath(window.location.hash.substr(1)); - let { state } = window.history; + let state = globalHistory.state || {}; return createReadOnlyObject({ pathname, search, hash, - state: (state && state.user) || null, - key: (state && state.key) || 'default' + state: state.usr || null, + key: state.key || 'default', + index: state.idx }); }; - let action = PopAction; - let location = getLocation(); - let blockers = createEvents(); - let listeners = createEvents(); + let ignoreNextPop = false; - // TODO: Support forceRefresh and do NOT notify listeners when used. + // TODO: Add reload arg let handleNavigation = (nextAction, nextLocation) => { if (__DEV__) { if (nextLocation.pathname.charAt(0) !== '/') { @@ -209,29 +263,42 @@ export const createHashHistory = ({ window = document.defaultView } = {}) => { } } + if (nextAction === PopAction && ignoreNextPop) { + ignoreNextPop = false; + return; + } + if (blockers.length) { blockers.call({ action: nextAction, location: nextLocation }); - if (nextAction === PopAction) { - // TODO: revert the POP + if (nextAction === PopAction && nextLocation.index != null) { + // Revert the POP + let n = location.index - nextLocation.index; + if (n) { + ignoreNextPop = true; + globalHistory.go(n); + } } } else { - let state = { user: nextLocation.state, key: nextLocation.key }; - // TODO: Handle relative paths? Or just warn about them? + let state = { + usr: nextLocation.state, + key: nextLocation.key, + idx: nextLocation.index + }; // TODO: Support different "hash types"? let url = '#' + createPath(nextLocation); if (nextAction === PushAction) { // try...catch because iOS limits us to 100 pushState calls :/ try { - window.history.pushState(state, null, url); + globalHistory.pushState(state, null, url); } catch (error) { // They are going to lose state here, but there is no real // way to warn them about it since the page will refresh... window.location.assign(url); } } else if (nextAction === ReplaceAction) { - window.history.replaceState(state, null, url); + globalHistory.replaceState(state, null, url); } action = nextAction; @@ -240,6 +307,15 @@ export const createHashHistory = ({ window = document.defaultView } = {}) => { } }; + let toggleBeforeUnloadBlocker = on => + window[`${on ? 'add' : 'remove'}EventListener`]( + BeforeUnloadEvent, + preventUnload + ); + + // Initialize the index for this document. + globalHistory.replaceState({ ...globalHistory.state, idx: 0 }, null); + window.addEventListener(PopStateEvent, event => { handleNavigation(PopAction, getLocation()); }); @@ -253,7 +329,18 @@ export const createHashHistory = ({ window = document.defaultView } = {}) => { } }); + let action = PopAction; + let location = getLocation(); + let blockers = createEvents(); + let listeners = createEvents(); + let history = { + get action() { + return action; + }, + get location() { + return location; + }, createHref: location => { let base = document.querySelector('base'); let href = ''; @@ -262,7 +349,17 @@ export const createHashHistory = ({ window = document.defaultView } = {}) => { } return href + '#' + createPath(location); }, - block: fn => blockers.push(fn), + block: fn => { + let unblock = blockers.push(fn); + if (blockers.length === 1) toggleBeforeUnloadBlocker(1); + return () => { + unblock(); + // Remove the beforeunload listener so the document may be + // salvageable in the pagehide event. + // See https://html.spec.whatwg.org/#unloading-documents + if (!blockers.length) toggleBeforeUnloadBlocker(0); + }; + }, listen: fn => listeners.push(fn), navigate: (to, { replace = false, state = null } = {}) => handleNavigation( @@ -272,24 +369,15 @@ export const createHashHistory = ({ window = document.defaultView } = {}) => { ? parsePath(to) : { pathname: '/', search: '', hash: '', ...to }), state, - key: createKey() + key: createKey(), + index: location.index + (replace ? 0 : 1) }) ), - go: n => window.history.go(n), + go: n => globalHistory.go(n), back: () => history.go(-1), forward: () => history.go(1) }; - Object.defineProperty(history, 'action', { - enumerable: true, - get: () => action - }); - - Object.defineProperty(history, 'location', { - enumerable: true, - get: () => location - }); - return history; }; @@ -354,3 +442,10 @@ const createEvents = () => { const clamp = (n, lowerBound, upperBound) => Math.min(Math.max(n, lowerBound), upperBound); + +const preventUnload = event => { + // Cancel the event. + event.preventDefault(); + // Chrome (and legacy IE) requires returnValue to be set. + event.returnValue = ''; +}; From 0831aae742c8146f7aaecebd2183078a899221bf Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 6 Nov 2019 11:59:11 -0800 Subject: [PATCH 009/133] Add history.retry for retrying transitions Also, wait to call blockers on POP transitions until we have already returned to the URL we just left. --- .size-snapshot.json | 18 ++--- fixtures/unload-history.html | 34 ++++----- modules/index.js | 135 +++++++++++++++++++++++++---------- 3 files changed, 119 insertions(+), 68 deletions(-) diff --git a/.size-snapshot.json b/.size-snapshot.json index ffc5b5dbd..a0cf97e3e 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -1,8 +1,8 @@ { "esm/history.js": { - "bundled": 16328, - "minified": 6304, - "gzipped": 1786, + "bundled": 18963, + "minified": 7493, + "gzipped": 2040, "treeshaked": { "rollup": { "code": 43, @@ -14,13 +14,13 @@ } }, "umd/history.js": { - "bundled": 17970, - "minified": 5813, - "gzipped": 1944 + "bundled": 20613, + "minified": 6873, + "gzipped": 2192 }, "umd/history.min.js": { - "bundled": 17356, - "minified": 5500, - "gzipped": 1813 + "bundled": 18495, + "minified": 5944, + "gzipped": 1893 } } diff --git a/fixtures/unload-history.html b/fixtures/unload-history.html index 11348a447..e263f95c8 100644 --- a/fixtures/unload-history.html +++ b/fixtures/unload-history.html @@ -11,12 +11,6 @@

    Controls

    -

    - regular links:
    - home - one - two -

    pushState & replaceState:
    pushState(home) @@ -24,6 +18,10 @@

    Controls

    pushState(two) replaceState(three)

    +

    + regular link:
    + home +

    Test Scenarios

    @@ -31,7 +29,6 @@

    Test Scenarios