diff --git a/.travis.yml b/.travis.yml index 8bba685..96d9d98 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: node_js node_js: - "8.9.0" before_install: - - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.3.2 + - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.5.1 - export PATH="$HOME/.yarn/bin:$PATH" script: - yarn run travis diff --git a/MIGRATIONS.md b/MIGRATIONS.md new file mode 100644 index 0000000..eced28c --- /dev/null +++ b/MIGRATIONS.md @@ -0,0 +1,52 @@ +## Migrating from 1.x.x -> 2.x.x + +> 1.) `withBreadcrumbs` is now the default export + +_1.x.x_ +```js +import { withBreadcrumbs } from 'react-router-breadcrumbs-hoc'; +``` + +_2.x.x_ +```js +import withBreadcrumbs from 'react-router-breadcrumbs-hoc'; +``` + +> 2.) The breadcrumbs array returned by the HOC is now _just_ the components. It _used_ to be an array of objects, but I decided this approach was easier to understand and made the implementation code a bit cleaner. + +_1.x.x_ +```js +{breadcrumbs.map(({ breadcrumb, path, match }) => ( + + + {breadcrumb} + + +))} +``` + +_2.x.x_ +```js +{breadcrumbs.map(breadcrumb => ( + + + {breadcrumb} + + +))} +``` + +> 3.) The package will now attempt to return sensible defaults for breadcrumbs unless otherwise provided making the the package now "opt-out" instead of "opt-in" for all paths. See the readme for how to disable default breadcrumb behavior. + +_1.x.x_ +```js +withBreadcrumbs([ + { path: '/', breadcrumb: 'Home' }, + { path: '/users', breadcrumb: 'Users' }, +])(Component); +``` + +_2.x.x_ (the above breadcrumbs will be automagically generated so there's no need to include them in config) +```js +withBreadcrumbs()(Component); +``` diff --git a/README.md b/README.md index b4b5d71..d1e3b79 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,11 @@

- Just a tiny, flexible, higher order component for rendering breadcrumbs with react-router 4.x + A tiny (~2kb minified), flexible, higher order component for rendering breadcrumbs with react-router 4.x

- site.com/user/id → user / John Doe + site.com/user/id → Home / User / John Doe

@@ -34,35 +34,36 @@ or ## Usage ```js -withBreadcrumbs(routeConfigObject)(MyComponent); +withBreadcrumbs()(MyComponent); ``` -## Example +## Simple example ```js import React from 'react'; import { NavLink } from 'react-router-dom'; -import { withBreadcrumbs } from 'react-router-breadcrumbs-hoc'; +import withBreadcrumbs from 'react-router-breadcrumbs-hoc'; +// breadcrumbs can be any type of component or string const UserBreadcrumb = ({ match }) => {match.params.userId}; // use match param userId to fetch/display user name +// define some custom breadcrumbs for certain routes (optional) const routes = [ - { path: '/', breadcrumb: 'Home' }, - { path: '/users', breadcrumb: 'Users' }, { path: '/users/:userId', breadcrumb: UserBreadcrumb }, - { path: '/something-else', breadcrumb: ':)' }, + { path: '/example', breadcrumb: 'Custom Example' }, ]; -// map & render your breadcrumb components however you want +// map & render your breadcrumb components however you want. +// each `breadcrumb` has the props `key`, `location`, and `match` included! const Breadcrumbs = ({ breadcrumbs }) => (

- {breadcrumbs.map(({ breadcrumb, path, match }) => ( - - + {breadcrumbs.map((breadcrumb, index) => ( + + {breadcrumb} - / + {(index < breadcrumbs.length - 1) && / } ))}
@@ -77,72 +78,79 @@ Pathname | Result --- | --- /users | Home / Users /users/id | Home / Users / John -/something-else | Home / :) +/example | Home / Custom Example + +## Disabling default breadcrumbs for paths + +This package will attempt to create breadcrumbs for you based on the route section via [humanize-string](https://github.com/sindresorhus/humanize-string). For example `/users` will auotmatically create the breadcrumb `"Users"`. There are two ways to disable default breadcrumbs for a path: + +1.) Pass `breadcrumb: null` in the routes config +`{ path: '/a/b', breadcrumb: null }` + +2.) Pass an `excludePaths` array in the `options` +`withBreadcrumbs(routes, { excludePaths: ['/', '/no-breadcrumb/for-this-route'] })` + +in your routes array. ## Already using a [route config](https://reacttraining.com/react-router/web/example/route-config) array with react-router? -Just add a `breadcrumbs` prop to your routes that require breadcrumbs! +Just add a `breadcrumbs` prop to your routes that require custom breadcrumbs. -> Note: currently nested `routes` arrays are not supported, but will be soon (see: https://github.com/icd2k3/react-router-breadcrumbs-hoc/issues/24) +> Note: currently, nested `route`s arrays are _not_ supported, but will be soon (see: https://github.com/icd2k3/react-router-breadcrumbs-hoc/issues/24) ## API ```js Route = { path: String - breadcrumb: String|Function + breadcrumb: String|Function? // note: if not provided, a default breadcrumb will be returned matchOptions?: Object } -Breadcrumb = { - path: String - match: String - breadcrumb: Component +Options = { + excludePaths: Array } -// react-router's location object: https://reacttraining.com/react-router/web/api/location -Location = { - key: String - pathname: String - search: String - hash: String - state: Object -} - -withBreadcrumbs(routes: Array): HigherOrderComponent +// if routes are not passed, default breadcrumbs will be returned +withBreadcrumbs(routes?: Array, options? Object): HigherOrderComponent // you shouldn't ever really have to use `getBreadcrumbs`, but it's // exported for convenience if you don't want to use the HOC -getBreadcrumbs({ routes: Array, location: Location }): Array +getBreadcrumbs({ + routes: Array, + location: Object, // react-router's location object: https://reacttraining.com/react-router/web/api/location + options: Object, +}): Array ``` -## Order Matters! +## Order matters! -Consider the following route config: +Consider the following route configs: ```js [ - { path: '/users', breadcrumb: 'Users' }, - { path: '/users/:id', breadcrumb: 'Users - id' }, - { path: '/users/create', breadcrumb: 'Users - create' }, + { path: '/users/:id', breadcrumb: 'id-breadcrumb' }, + { path: '/users/create', breadcrumb: 'create-breadcrumb' }, ] -``` -This package acts like a switch statement and matches the first breadcrumb it can find. So, unfortunately, visiting `/users/create` will result in the `Users > Users - id` breadcrumbs instead of the desired `Users > Users - create` breadcrumbs. +// example.com/users/create = 'id-breadcrumb' (because path: '/users/:id' will match first) +// example.com/users/123 = 'id-breadcumb' +``` -To get the right breadcrumbs, simply change the order: +To fix the issue above, just adjust the order of your routes: ```js [ - { path: '/users', breadcrumb: 'Users' }, - { path: '/users/create', breadcrumb: 'Users - create' }, - { path: '/users/:id', breadcrumb: 'Users - id' }, + { path: '/users/create', breadcrumb: 'create-breadcrumb' }, + { path: '/users/:id', breadcrumb: 'id-breadcrumb' }, ] + +// example.com/users/create = 'create-breadcrumb' (because path: '/users/create' will match first) +// example.com/users/123 = 'id-breadcrumb' ``` -Now, `/users/create` will match the create breadcrumb first, and all others will fall through to `/:id`. +## Using the location object -## Using the Location Object React Router's [location](https://reacttraining.com/react-router/web/api/location) object lets you pass `state` property. Using the `state` allows one to update the Breadcrumb to display dynamic info at runtime. Consider this example: ```jsx diff --git a/jest.config.js b/jest.config.js index 192c92a..bd31af3 100644 --- a/jest.config.js +++ b/jest.config.js @@ -16,5 +16,4 @@ module.exports = { }, }, setupFiles: ['./jest.setup.js'], - snapshotSerializers: ['enzyme-to-json/serializer'], }; diff --git a/package.json b/package.json index 2bd37f4..3f48cf3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-router-breadcrumbs-hoc", - "version": "1.2.0", + "version": "2.0.0", "description": "Just a tiny, flexible, higher order component for rendering breadcrumbs with react-router 4.x", "repository": "icd2k3/react-router-breadcrumbs-hoc", "keywords": [ @@ -19,6 +19,9 @@ "react-router": "^4.2.0", "react-router-dom": "^4.2.2" }, + "dependencies": { + "humanize-string": "^1.0.1" + }, "devDependencies": { "babel-cli": "^6.26.0", "babel-core": "^6.8.0", @@ -30,11 +33,9 @@ "babel-preset-react": "^6.24.1", "babel-preset-stage-1": "^6.24.1", "coveralls": "^3.0.0", - "cross-env": "^5.1.3", "enzyme": "^3.2.0", "enzyme-adapter-react-16": "^1.1.0", - "enzyme-to-json": "^3.3.0", - "eslint": "^4.18.1", + "eslint": "^4.18.2", "eslint-config-airbnb": "^16.1.0", "eslint-plugin-import": "^2.9.0", "eslint-plugin-jsx-a11y": "^6.0.3", @@ -45,16 +46,16 @@ "react-dom": "^16.2.0", "react-router": "^4.2.0", "react-router-dom": "^4.2.2", - "rollup": "^0.56.3", + "rollup": "^0.56.5", "rollup-plugin-babel": "^3.0.3", - "rollup-plugin-commonjs": "^8.2.6", - "rollup-plugin-node-resolve": "^3.0.2" + "rollup-plugin-commonjs": "^9.0.0", + "rollup-plugin-node-resolve": "^3.2.0" }, "scripts": { "prepublishOnly": "npm run build", "build": "rollup -c", - "test": "cross-env NODE_ENV=test jest", - "travis": "jest && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", + "test": "jest", + "travis": "yarn run lint && jest && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", "lint": "eslint ./src/**" } } diff --git a/rollup.config.js b/rollup.config.js index 34d3e4e..c044014 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -13,6 +13,7 @@ const globals = { const config = { input: 'src/index.js', + exports: 'named', plugins: [ babel({ exclude: 'node_modules/**', diff --git a/src/__snapshots__/index.test.js.snap b/src/__snapshots__/index.test.js.snap deleted file mode 100644 index 130c4ea..0000000 --- a/src/__snapshots__/index.test.js.snap +++ /dev/null @@ -1,606 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`react-router-breadcrumbs-hoc Custom match options Should render empty container 1`] = ` - - - - -
- - 1 - - - 1 - -
-
-
-
-
-`; - -exports[`react-router-breadcrumbs-hoc No matching routes Should render empty container 1`] = ` - - - - -
- - - - -`; - -exports[`react-router-breadcrumbs-hoc Valid routes Should render breadcrumb components as expected 1`] = ` - - - - , - "match": Object { - "isExact": true, - "params": Object {}, - "path": "/1/2", - "url": "/1/2", - }, - "path": "/1/2", - }, - Object { - "breadcrumb": , - "match": Object { - "isExact": true, - "params": Object { - "number": "3", - }, - "path": "/1/2/:number", - "url": "/1/2/3", - }, - "path": "/1/2/:number", - }, - Object { - "breadcrumb": , - "match": Object { - "isExact": true, - "params": Object { - "number": "3", - }, - "path": "/1/2/:number/4", - "url": "/1/2/3/4", - }, - "path": "/1/2/:number/4", - }, - ] - } - history={ - Object { - "action": "POP", - "block": [Function], - "createHref": [Function], - "go": [Function], - "goBack": [Function], - "goForward": [Function], - "listen": [Function], - "location": Object { - "hash": "", - "pathname": "/1/2/3/4", - "search": "", - }, - "push": [Function], - "replace": [Function], - } - } - location={ - Object { - "hash": "", - "pathname": "/1/2/3/4", - "search": "", - } - } - match={ - Object { - "isExact": false, - "params": Object {}, - "path": "/", - "url": "/", - } - } - staticContext={Object {}} - > -
- - Home - - - 1 - - - - - TWO - - - - - - - 3 - - - - - - - - - - Link - - - - - - -
-
-
-
-
-`; - -exports[`react-router-breadcrumbs-hoc When extending react-router config Should render expected breadcrumbs and omit routes that do not require them 1`] = ` - - - - -
- - - - -`; diff --git a/src/index.js b/src/index.js index 4ab3eb5..0b1406c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,61 +1,117 @@ import { createElement } from 'react'; import { matchPath, withRouter } from 'react-router'; +import humanizeString from 'humanize-string'; const DEFAULT_MATCH_OPTIONS = { exact: true }; +const NO_BREADCRUMB = 'NO_BREADCRUMB'; -// if user is passing a function (component) as a breadcrumb, make sure we -// pass the match object into it. Else just return the string. -const renderer = ({ breadcrumb, match, location }) => { +// renders and returns the breadcrumb complete with `match`, `location`, and `key` props +const render = ({ breadcrumb, match, location }) => { + const componentProps = { match, location, key: match.path }; if (typeof breadcrumb === 'function') { - return createElement(breadcrumb, { match, location }); + return createElement(breadcrumb, componentProps); } - return breadcrumb; + return createElement('span', componentProps, breadcrumb); }; -export const getBreadcrumbs = ({ routes, location }) => { +// small helper method to get a default `humanize-string` breadcrumb if the +// user hasn't provided one +const getDefaultBreadcrumb = ({ pathSection, currentSection, location }) => { + const match = matchPath(pathSection, { ...DEFAULT_MATCH_OPTIONS, path: pathSection }); + + return render({ + breadcrumb: humanizeString(currentSection), + match, + location, + }); +}; + +// loops through the route array (if provided) and returns either +// a user-provided breadcrumb OR a sensible default via `humanize-string` +const getBreadcrumb = ({ + currentSection, + excludePaths, + location, + pathSection, + routes, +}) => { + let breadcrumb; + + // check the optional `exludePaths` option in `options` to see if the + // current path should not include a breadcrumb + if (excludePaths && excludePaths.includes(pathSection)) { + return NO_BREADCRUMB; + } + + // loop through the route array and see if the user has provided a custom breadcrumb + routes.some(({ breadcrumb: userProvidedBreadcrumb, matchOptions, path }) => { + if (!path) { + throw new Error('withBreadcrumbs: `path` must be provided in every route object'); + } + + const match = matchPath(pathSection, { ...(matchOptions || DEFAULT_MATCH_OPTIONS), path }); + + // if user passed breadcrumb: null OR custom match options to suppress a breadcrumb + // we need to know NOT to add it to the matches array + // see: `if (breadcrumb !== NO_BREADCRUMB)` below + if ((match && userProvidedBreadcrumb === null) || (!match && matchOptions)) { + breadcrumb = NO_BREADCRUMB; + return true; + } + + if (match) { + breadcrumb = render({ + // although we have a match, the user may be passing their react-router config object + // which we support. The route config object may not have a `breadcrumb` param specified. + // If this is the case, we should provide a default via `humanizeString` + breadcrumb: userProvidedBreadcrumb || humanizeString(currentSection), + match, + location, + }); + return true; + } + return false; + }); + + // if there are no breadcrumbs provided in the routes array we return a default breadcrumb instead + return breadcrumb + || getDefaultBreadcrumb({ + pathSection, + // include a "Home" breadcrumb by default (can be overrode or disabled in config) + currentSection: pathSection === '/' ? 'Home' : currentSection, + location, + }); +}; + +export const getBreadcrumbs = ({ routes, location, options = {} }) => { const matches = []; const { pathname } = location; pathname - // remove trailing slash "/" from pathname (avoids multiple of the same match) + .split('?')[0] + // remove trailing slash "/" from pathname .replace(/\/$/, '') // split pathname into sections .split('/') // reduce over the sections and find matches from `routes` prop - .reduce((previous, current) => { - // combine the last route section with the current + .reduce((previousSection, currentSection) => { + // combine the last route section with the currentSection // ex `pathname = /1/2/3 results in match checks for // `/1`, `/1/2`, `/1/2/3` - const pathSection = !current ? '/' : `${previous}/${current}`; - - let breadcrumbMatch; - - routes.some(({ breadcrumb, matchOptions, path }) => { - if (!path) { - throw new Error('withBreadcrumbs: `path` must be provided in every route object'); - } - if (!breadcrumb) { - return false; - } - const match = matchPath(pathSection, { ...(matchOptions || DEFAULT_MATCH_OPTIONS), path }); - - // if a route match is found ^ break out of the loop with a rendered breadcumb - // and match object to add to the `matches` array - if (match) { - breadcrumbMatch = { - breadcrumb: renderer({ breadcrumb, match, location }), - path, - match, - }; - return true; - } - - return false; + const pathSection = !currentSection ? '/' : `${previousSection}/${currentSection}`; + + const breadcrumb = getBreadcrumb({ + currentSection, + excludePaths: options.excludePaths, + location, + pathSection, + routes, }); - /* istanbul ignore else */ - if (breadcrumbMatch) { - matches.push(breadcrumbMatch); + // add the breadcrumb to the matches array + // unless the user has explicitly passed { path: x, breadcrumb: null } to disable + if (breadcrumb !== NO_BREADCRUMB) { + matches.push(breadcrumb); } return pathSection === '/' ? '' : pathSection; @@ -64,11 +120,14 @@ export const getBreadcrumbs = ({ routes, location }) => { return matches; }; -export const withBreadcrumbs = routes => Component => withRouter(props => +const withBreadcrumbs = (routes = [], options) => Component => withRouter(props => createElement(Component, { ...props, breadcrumbs: getBreadcrumbs({ routes, location: props.location, + options, }), })); + +export default withBreadcrumbs; diff --git a/src/index.test.js b/src/index.test.js index ab7ff55..e5106c1 100644 --- a/src/index.test.js +++ b/src/index.test.js @@ -3,20 +3,44 @@ import React from 'react'; import PropTypes from 'prop-types'; import { mount } from 'enzyme'; -import { StaticRouter as Router } from 'react-router'; +import { MemoryRouter as Router } from 'react-router'; import { NavLink } from 'react-router-dom'; -import { getBreadcrumbs, withBreadcrumbs } from './index'; +import withBreadcrumbs, { getBreadcrumbs } from './index'; const components = { Breadcrumbs: ({ breadcrumbs }) => ( -
- {breadcrumbs.map(({ breadcrumb, path }, index) => ( - {breadcrumb} +

+ {breadcrumbs.map((breadcrumb, index) => ( + + {breadcrumb} + {(index < breadcrumbs.length - 1) && / } + ))} -

+ ), BreadcrumbMatchTest: ({ match }) => {match.params.number}, BreadcrumbNavLinkTest: ({ match }) => Link, + BreadcrumbLocationTest: ({ location: { state: { isLocationTest } } }) => ( + + {isLocationTest ? 'pass' : 'fail'} + + ), +}; + +const render = ({ + options, + pathname, + routes, + state, +}) => { + const Breadcrumbs = withBreadcrumbs(routes, options)(components.Breadcrumbs); + const wrapper = + mount(); + + return { + Breadcrumbs: wrapper.find('.breadcrumbs-container'), + wrapper, + }; }; const matchShape = { @@ -27,10 +51,7 @@ const matchShape = { }; components.Breadcrumbs.propTypes = { - breadcrumbs: PropTypes.arrayOf(PropTypes.shape({ - match: PropTypes.shape(matchShape).isRequired, - path: PropTypes.string.isRequired, - })).isRequired, + breadcrumbs: PropTypes.arrayOf(PropTypes.node).isRequired, }; components.BreadcrumbMatchTest.propTypes = { @@ -41,91 +62,158 @@ components.BreadcrumbNavLinkTest.propTypes = { match: PropTypes.shape(matchShape).isRequired, }; +components.BreadcrumbLocationTest.propTypes = { + location: PropTypes.shape({ + state: PropTypes.shape({ + isLocationTest: PropTypes.bool.isRequired, + }).isRequired, + }).isRequired, +}; + describe('react-router-breadcrumbs-hoc', () => { describe('Valid routes', () => { - const routes = [ - // test home route - { path: '/', breadcrumb: 'Home' }, - // test breadcrumb passed as string - { path: '/1', breadcrumb: '1' }, - // test simple breadcrumb component - { path: '/1/2', breadcrumb: () => TWO }, - // test advanced breadcrumb component (user can use `match` however they wish) - { path: '/1/2/:number', breadcrumb: components.BreadcrumbMatchTest }, - // test NavLink wrapped breadcrumb - { path: '/1/2/:number/4', breadcrumb: components.BreadcrumbNavLinkTest }, - // test a no-match route - { path: '/no-match', breadcrumb: 'no match' }, - ]; - const routerProps = { - context: {}, - location: { pathname: '/1/2/3/4' }, - }; - it('Should render breadcrumb components as expected', () => { - const ComposedComponent = withBreadcrumbs(routes)(components.Breadcrumbs); - const wrapper = mount(); - - expect(wrapper.find(ComposedComponent)).toMatchSnapshot(); + const routes = [ + // test home route + { path: '/', breadcrumb: 'Home' }, + // test breadcrumb passed as string + { path: '/1', breadcrumb: 'One' }, + // test simple breadcrumb component + { path: '/1/2', breadcrumb: () => TWO }, + // test advanced breadcrumb component (user can use `match` however they wish) + { path: '/1/2/:number', breadcrumb: components.BreadcrumbMatchTest }, + // test NavLink wrapped breadcrumb + { path: '/1/2/:number/4', breadcrumb: components.BreadcrumbNavLinkTest }, + // test a no-match route + { path: '/no-match', breadcrumb: 'no match' }, + ]; + const { Breadcrumbs, wrapper } = render({ pathname: '/1/2/3/4', routes }); + expect(Breadcrumbs.text()).toBe('Home / One / TWO / 3 / Link'); expect(wrapper.find(NavLink).props().to).toBe('/1/2/3/4'); }); }); - describe('No matching routes', () => { - const routes = [ - { path: '/1', breadcrumb: '1' }, - ]; - const routerProps = { - context: {}, - location: { pathname: 'nope' }, - }; - - it('Should render empty container', () => { - const ComposedComponent = withBreadcrumbs(routes)(components.Breadcrumbs); - const wrapper = mount(); + describe('Route order', () => { + it('Should match the first breadcrumb in route array user/create', () => { + const routes = [ + { + path: '/user/create', + breadcrumb: 'Add User', + }, + { + path: '/user/:id', + breadcrumb: '1', + }, + ]; + const { Breadcrumbs } = render({ pathname: '/user/create', routes }); + expect(Breadcrumbs.text()).toBe('Home / User / Add User'); + }); - expect(wrapper.find(ComposedComponent)).toMatchSnapshot(); + it('Should match the first breadcrumb in route array user/:id', () => { + const routes = [ + { + path: '/user/:id', + breadcrumb: 'Oops', + }, + { + path: '/user/create', + breadcrumb: 'Add User', + }, + ]; + const { Breadcrumbs } = render({ pathname: '/user/create', routes }); + expect(Breadcrumbs.text()).toBe('Home / User / Oops'); }); }); describe('Custom match options', () => { - const routes = [ - { - path: '/1', - breadcrumb: '1', - // not recommended, but supported - matchOptions: { exact: false, strict: true }, - }, - ]; - const routerProps = { - context: {}, - location: { pathname: '/1/2' }, - }; - - it('Should render empty container', () => { - const ComposedComponent = withBreadcrumbs(routes)(components.Breadcrumbs); - const wrapper = mount(); - - expect(wrapper.find(ComposedComponent)).toMatchSnapshot(); + it('Should allow `strict` rule', () => { + const routes = [ + { + path: '/one/', + breadcrumb: '1', + // not recommended, but supported + matchOptions: { exact: false, strict: true }, + }, + ]; + const { Breadcrumbs } = render({ pathname: '/one', routes }); + expect(Breadcrumbs.text()).toBe(''); }); }); describe('When extending react-router config', () => { - const routes = [ - { path: '/1', breadcrumb: '1' }, - // no breadcrumb required for this route - { path: '/2' }, - ]; - const routerProps = { - context: {}, - location: { pathname: '/2' }, - }; - - it('Should render expected breadcrumbs and omit routes that do not require them', () => { - const ComposedComponent = withBreadcrumbs(routes)(components.Breadcrumbs); - const wrapper = mount(); - - expect(wrapper.find(ComposedComponent)).toMatchSnapshot(); + it('Should render expected breadcrumbs with sensible defaults', () => { + const routes = [ + { path: '/one', breadcrumb: 'OneCustom' }, + { path: '/one/two' }, + ]; + const { Breadcrumbs } = render({ pathname: '/one/two', routes }); + expect(Breadcrumbs.text()).toBe('Home / OneCustom / Two'); + }); + }); + + describe('Defaults', () => { + describe('No routes array', () => { + it('Should automatically render breadcrumbs with default strings', () => { + const { Breadcrumbs } = render({ pathname: '/one/two' }); + + expect(Breadcrumbs.text()).toBe('Home / One / Two'); + }); + }); + + describe('Override defaults', () => { + it('Should render user-provided breadcrumbs where possible and use defaults otherwise', () => { + const routes = [{ path: '/one', breadcrumb: 'Override' }]; + const { Breadcrumbs } = render({ pathname: '/one/two', routes }); + + expect(Breadcrumbs.text()).toBe('Home / Override / Two'); + }); + }); + + describe('No breadcrumb', () => { + it('Should be possible to NOT render a breadcrumb', () => { + const routes = [{ path: '/one', breadcrumb: null }]; + const { Breadcrumbs } = render({ pathname: '/one/two', routes }); + + expect(Breadcrumbs.text()).toBe('Home / Two'); + }); + + it('Should be possible to NOT render a "Home" breadcrumb', () => { + const routes = [{ path: '/', breadcrumb: null }]; + const { Breadcrumbs } = render({ pathname: '/one/two', routes }); + + expect(Breadcrumbs.text()).toBe('One / Two'); + }); + }); + }); + + describe('When using the location object', () => { + it('Should be provided in the rendered breadcrumb component', () => { + const routes = [{ path: '/one', breadcrumb: components.BreadcrumbLocationTest }]; + const { Breadcrumbs } = render({ pathname: '/one', state: { isLocationTest: true }, routes }); + expect(Breadcrumbs.text()).toBe('Home / pass'); + }); + }); + + describe('When pathname includes query params', () => { + it('Should not render query breadcrumb', () => { + const { Breadcrumbs } = render({ pathname: '/one?mock=query' }); + expect(Breadcrumbs.text()).toBe('Home / One'); + }); + }); + + describe('When pathname includes a trailing slash', () => { + it('Should ignore the trailing slash', () => { + const { Breadcrumbs } = render({ pathname: '/one/' }); + expect(Breadcrumbs.text()).toBe('Home / One'); + }); + }); + + describe('Options', () => { + describe('excludePaths', () => { + it('Should not return breadcrumbs for specified paths', () => { + const { Breadcrumbs } = render({ pathname: '/one/two', options: { excludePaths: ['/', '/one'] } }); + expect(Breadcrumbs.text()).toBe('Two'); + }); }); }); diff --git a/yarn.lock b/yarn.lock index ec4af42..5952640 100644 --- a/yarn.lock +++ b/yarn.lock @@ -99,13 +99,13 @@ acorn@^3.0.4: version "3.3.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" -acorn@^5.0.0, acorn@^5.2.1, acorn@^5.3.0, acorn@^5.4.0: +acorn@^5.0.0, acorn@^5.3.0, acorn@^5.4.0: version "5.5.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.0.tgz#1abb587fbf051f94e3de20e6b26ef910b1828298" -ajv-keywords@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.1.0.tgz#ac2b27939c543e95d2c06e7f7f5c27be4aa543be" +ajv-keywords@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" ajv@^4.9.1: version "4.11.8" @@ -114,7 +114,7 @@ ajv@^4.9.1: co "^4.6.0" json-stable-stringify "^1.0.1" -ajv@^5.1.0, ajv@^5.3.0: +ajv@^5.1.0, ajv@^5.2.3, ajv@^5.3.0: version "5.5.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" dependencies: @@ -123,14 +123,6 @@ ajv@^5.1.0, ajv@^5.3.0: fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.3.0" -ajv@^6.0.1: - version "6.2.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.2.0.tgz#afac295bbaa0152449e522742e4547c1ae9328d2" - dependencies: - fast-deep-equal "^1.0.0" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.3.0" - align-text@^0.1.1, align-text@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" @@ -1108,10 +1100,14 @@ bser@^2.0.0: dependencies: node-int64 "^0.4.0" -builtin-modules@^1.0.0, builtin-modules@^1.1.0, builtin-modules@^1.1.1: +builtin-modules@^1.0.0, builtin-modules@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" +builtin-modules@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-2.0.0.tgz#60b7ef5ae6546bd7deefa74b08b62a43a232648e" + caller-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" @@ -1313,13 +1309,6 @@ coveralls@^3.0.0: minimist "^1.2.0" request "^2.79.0" -cross-env@^5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.1.3.tgz#f8ae18faac87692b0a8b4d2f7000d4ec3a85dfd7" - dependencies: - cross-spawn "^5.1.0" - is-windows "^1.0.0" - cross-spawn@^5.0.1, cross-spawn@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -1550,12 +1539,6 @@ enzyme-adapter-utils@^1.3.0: object.assign "^4.0.4" prop-types "^15.6.0" -enzyme-to-json@^3.3.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/enzyme-to-json/-/enzyme-to-json-3.3.1.tgz#64239dcd417e2fb552f4baa6632de4744b9b5b93" - dependencies: - lodash "^4.17.4" - enzyme@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.3.0.tgz#0971abd167f2d4bf3f5bd508229e1c4b6dc50479" @@ -1693,9 +1676,9 @@ eslint-visitor-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" -eslint@^4.18.1: - version "4.18.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.18.1.tgz#b9138440cb1e98b2f44a0d578c6ecf8eae6150b0" +eslint@^4.18.2: + version "4.18.2" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.18.2.tgz#0f81267ad1012e7d2051e186a9004cc2267b8d45" dependencies: ajv "^5.3.0" babel-code-frame "^6.22.0" @@ -1732,7 +1715,7 @@ eslint@^4.18.1: semver "^5.3.0" strip-ansi "^4.0.0" strip-json-comments "~2.0.1" - table "^4.0.1" + table "4.0.2" text-table "~0.2.0" espree@^3.5.2: @@ -1774,7 +1757,7 @@ estree-walker@^0.3.0: version "0.3.1" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.3.1.tgz#e6b1a51cf7292524e7237c312e5fe6660c1ce1aa" -estree-walker@^0.5.0: +estree-walker@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.5.1.tgz#64fc375053abc6f57d73e9bd2f004644ad3c5854" @@ -2243,6 +2226,12 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" +humanize-string@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/humanize-string/-/humanize-string-1.0.1.tgz#fce2d6c545efc25dea1f23235182c98da0180b42" + dependencies: + decamelize "^1.0.0" + iconv-lite@0.4.19, iconv-lite@^0.4.17, iconv-lite@~0.4.13: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" @@ -2470,10 +2459,6 @@ is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" -is-windows@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -3862,7 +3847,7 @@ resolve@1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" -resolve@^1.1.6, resolve@^1.4.0, resolve@^1.5.0: +resolve@^1.1.6, resolve@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36" dependencies: @@ -3897,21 +3882,20 @@ rollup-plugin-babel@^3.0.3: dependencies: rollup-pluginutils "^1.5.0" -rollup-plugin-commonjs@^8.2.6: - version "8.3.0" - resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-8.3.0.tgz#91b4ba18f340951e39ed7b1901f377a80ab3f9c3" +rollup-plugin-commonjs@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.0.0.tgz#9118e9041b67cdd2a6d8c59dda6349556dce3d0c" dependencies: - acorn "^5.2.1" - estree-walker "^0.5.0" + estree-walker "^0.5.1" magic-string "^0.22.4" - resolve "^1.4.0" + resolve "^1.5.0" rollup-pluginutils "^2.0.1" -rollup-plugin-node-resolve@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-3.0.3.tgz#8f57b253edd00e5b0ad0aed7b7e9cf5982e98fa4" +rollup-plugin-node-resolve@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-3.2.0.tgz#31534952f3ab21f9473c1d092be7ed43937ea4d4" dependencies: - builtin-modules "^1.1.0" + builtin-modules "^2.0.0" is-module "^1.0.0" resolve "^1.1.6" @@ -3929,9 +3913,9 @@ rollup-pluginutils@^2.0.1: estree-walker "^0.3.0" micromatch "^2.3.11" -rollup@^0.56.3: - version "0.56.3" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.56.3.tgz#7900695531afa1badd3235f285cc4aa0d49ce254" +rollup@^0.56.5: + version "0.56.5" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.56.5.tgz#40fe3cf0cd1659d469baad11f4d5b6336c14ce84" rst-selector-parser@^2.2.3: version "2.2.3" @@ -4190,12 +4174,12 @@ symbol-tree@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" -table@^4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/table/-/table-4.0.3.tgz#00b5e2b602f1794b9acaf9ca908a76386a7813bc" +table@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36" dependencies: - ajv "^6.0.1" - ajv-keywords "^3.0.0" + ajv "^5.2.3" + ajv-keywords "^2.1.0" chalk "^2.1.0" lodash "^4.17.4" slice-ansi "1.0.0" @@ -4263,8 +4247,8 @@ to-fast-properties@^2.0.0: resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" tough-cookie@>=2.3.3, tough-cookie@^2.3.3, tough-cookie@~2.3.0, tough-cookie@~2.3.3: - version "2.3.4" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" + version "2.3.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" dependencies: punycode "^1.4.1"