From 93aac6dc9b6926e901db41e0509d1cab6a9ef98b Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Fri, 18 Dec 2015 18:43:53 +0100 Subject: [PATCH] SONAR-7147 keep current state in the url --- server/sonar-web/package.json | 3 + .../src/main/js/apps/code/actions/index.js | 92 ++++-- server/sonar-web/src/main/js/apps/code/app.js | 19 +- .../src/main/js/apps/code/components/Code.js | 32 +- .../js/apps/code/components/SourceViewer.js | 8 + .../src/main/js/apps/code/reducers/index.js | 137 ++++----- .../main/js/apps/code/store/configureStore.js | 15 +- server/sonar-web/src/main/js/main/app.js | 3 +- .../tests/apps/code/components-test.js | 5 +- .../sonar-web/tests/apps/code/store-test.js | 279 ++++++++++++------ 10 files changed, 363 insertions(+), 230 deletions(-) diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index a698746b3d2..3c2e132b09e 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -42,6 +42,7 @@ "gulp-util": "3.0.6", "handlebars": "^2.0.0", "handlebars-loader": "^1.1.4", + "history": "^1.13.1", "imports-loader": "^0.6.5", "isparta": "^4.0.0", "jquery": "2.1.4", @@ -53,9 +54,11 @@ "react-addons-test-utils": "0.14.2", "react-dom": "0.14.2", "react-redux": "^4.0.1", + "react-router": "^1.0.2", "react-select": "1.0.0-beta6", "redux": "^3.0.5", "redux-logger": "^2.2.1", + "redux-simple-router": "^1.0.1", "redux-thunk": "^1.0.2", "script-loader": "^0.6.1", "sinon": "1.15.4", diff --git a/server/sonar-web/src/main/js/apps/code/actions/index.js b/server/sonar-web/src/main/js/apps/code/actions/index.js index 135c77758ab..dcf7e47d554 100644 --- a/server/sonar-web/src/main/js/apps/code/actions/index.js +++ b/server/sonar-web/src/main/js/apps/code/actions/index.js @@ -1,6 +1,8 @@ import _ from 'underscore'; +import { pushPath } from 'redux-simple-router'; import { getChildren, getComponent } from '../../../api/components'; +import { getComponentNavigation } from '../../../api/nav'; const METRICS = [ @@ -9,6 +11,7 @@ const METRICS = [ 'violations', 'duplicated_lines_density' ]; + const METRICS_WITH_COVERAGE = [ ...METRICS, 'coverage', @@ -19,60 +22,85 @@ const METRICS_WITH_COVERAGE = [ export const INIT = 'INIT'; export const BROWSE = 'BROWSE'; -export const RECEIVE_COMPONENTS = 'RECEIVE_COMPONENTS'; -export const SHOW_SOURCE = 'SHOW_SOURCE'; +export const START_FETCHING = 'START_FETCHING'; +export const STOP_FETCHING = 'STOP_FETCHING'; -export function requestComponents (baseComponent) { +export function initComponentAction (component, breadcrumbs = []) { return { - type: BROWSE, - baseComponent + type: INIT, + component, + breadcrumbs }; } - -export function receiveComponents (baseComponent, components) { +export function browseAction (component, children = [], breadcrumbs = []) { return { - type: RECEIVE_COMPONENTS, - baseComponent, - components + type: BROWSE, + component, + children, + breadcrumbs }; } +export function startFetching () { + return { type: START_FETCHING }; +} -export function showSource (component) { - return { - type: SHOW_SOURCE, - component - }; +export function stopFetching () { + return { type: STOP_FETCHING }; } -function fetchChildren (dispatch, getState, baseComponent) { - dispatch(requestComponents(baseComponent)); +function getPath (componentKey) { + return '/' + encodeURIComponent(componentKey); +} - const { coverageMetric } = getState(); - const metrics = [...METRICS, coverageMetric]; +function retrieveComponentBase (componentKey, candidate) { + return candidate ? + Promise.resolve(candidate) : + getComponent(componentKey, METRICS_WITH_COVERAGE); +} - return getChildren(baseComponent.key, metrics) - .then(components => _.sortBy(components, 'name')) - .then(components => dispatch(receiveComponents(baseComponent, components))); +function retrieveComponentChildren (componentKey, candidate) { + return candidate && candidate.children ? + Promise.resolve(candidate.children) : + getChildren(componentKey, METRICS_WITH_COVERAGE); } +function retrieveComponentBreadcrumbs (componentKey, candidate) { + return candidate && candidate.breadcrumbs ? + Promise.resolve(candidate.breadcrumbs) : + getComponentNavigation(componentKey).then(navigation => navigation.breadcrumbs); +} -export function initComponent (baseComponent) { - return (dispatch, getState) => { - return getComponent(baseComponent.key, METRICS_WITH_COVERAGE) - .then(component => fetchChildren(dispatch, getState, component)); - }; +function retrieveComponent (componentKey, bucket) { + const candidate = _.findWhere(bucket, { key: componentKey }); + return Promise.all([ + retrieveComponentBase(componentKey, candidate), + retrieveComponentChildren(componentKey, candidate), + retrieveComponentBreadcrumbs(componentKey, candidate) + ]); } +export function initComponent (componentKey, breadcrumbs) { + return dispatch => { + dispatch(startFetching()); + return getComponent(componentKey, METRICS_WITH_COVERAGE) + .then(component => dispatch(initComponentAction(component, breadcrumbs))) + .then(() => dispatch(stopFetching())); + }; +} -export function fetchComponents (baseComponent) { +export function browse (componentKey) { return (dispatch, getState) => { - const { fetching } = getState(); - if (!fetching) { - return fetchChildren(dispatch, getState, baseComponent); - } + const { bucket } = getState(); + dispatch(startFetching()); + return retrieveComponent(componentKey, bucket) + .then(([component, children, breadcrumbs]) => { + dispatch(browseAction(component, children, breadcrumbs)); + }) + .then(() => dispatch(pushPath(getPath(componentKey)))) + .then(() => dispatch(stopFetching())); }; } diff --git a/server/sonar-web/src/main/js/apps/code/app.js b/server/sonar-web/src/main/js/apps/code/app.js index c16b4aa3057..ce973644f69 100644 --- a/server/sonar-web/src/main/js/apps/code/app.js +++ b/server/sonar-web/src/main/js/apps/code/app.js @@ -1,18 +1,33 @@ import React from 'react'; import { render } from 'react-dom'; import { Provider } from 'react-redux'; +import { Router, Route } from 'react-router'; +import { createHashHistory } from 'history'; +import { syncReduxAndRouter } from 'redux-simple-router'; import Code from './components/Code'; import configureStore from './store/configureStore'; const store = configureStore(); +const history = createHashHistory({ + queryKey: false +}); + +syncReduxAndRouter(history, store); + +window.sonarqube.appStarted.then(({ el, component }) => { + const CodeWithComponent = () => { + return ; + }; -window.sonarqube.appStarted.then(({ el, ...other }) => { render( - + + + + , document.querySelector(el)); }); diff --git a/server/sonar-web/src/main/js/apps/code/components/Code.js b/server/sonar-web/src/main/js/apps/code/components/Code.js index ff463ebf5d0..7f364cf3939 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Code.js +++ b/server/sonar-web/src/main/js/apps/code/components/Code.js @@ -5,34 +5,31 @@ import { connect } from 'react-redux'; import Components from './Components'; import Breadcrumbs from './Breadcrumbs'; import SourceViewer from './SourceViewer'; -import { initComponent, fetchComponents, showSource } from '../actions'; +import { initComponent, browse } from '../actions'; import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin'; class Code extends Component { componentDidMount () { - const { dispatch, component } = this.props; - dispatch(initComponent(component)); + const { dispatch, component, routing } = this.props; + const selectedKey = (routing.path && decodeURIComponent(routing.path.substr(1))) || component.key; + dispatch(initComponent(component.key, component.breadcrumbs)) + .then(() => dispatch(browse(selectedKey))); } componentWillReceiveProps (nextProps) { - if (nextProps.component !== this.props.component) { - const { dispatch, component } = this.props; - dispatch(initComponent(component)); + if (nextProps.routing !== this.props.routing) { + const { dispatch, routing, component, fetching } = nextProps; + if (!fetching) { + const selectedKey = (routing.path && decodeURIComponent(routing.path.substr(1))) || component.key; + dispatch(browse(selectedKey)); + } } } - hasSourceCode (component) { - return component.qualifier === 'FIL' || component.qualifier === 'UTS'; - } - handleBrowse (component) { const { dispatch } = this.props; - if (this.hasSourceCode(component)) { - dispatch(showSource(component)); - } else { - dispatch(fetchComponents(component)); - } + dispatch(browse(component.key)); } render () { @@ -83,5 +80,6 @@ class Code extends Component { } } - -export default connect(state => state)(Code); +export default connect(state => { + return Object.assign({ routing: state.routing }, state.current); +})(Code); diff --git a/server/sonar-web/src/main/js/apps/code/components/SourceViewer.js b/server/sonar-web/src/main/js/apps/code/components/SourceViewer.js index f207e4a0faf..09457d8dd06 100644 --- a/server/sonar-web/src/main/js/apps/code/components/SourceViewer.js +++ b/server/sonar-web/src/main/js/apps/code/components/SourceViewer.js @@ -8,6 +8,14 @@ export default class SourceViewer extends Component { this.renderSourceViewer(); } + shouldComponentUpdate (nextProps) { + return nextProps.component.uuid !== this.props.component.uuid; + } + + componentWillUpdate () { + this.destroySourceViewer(); + } + componentDidUpdate () { this.renderSourceViewer(); } diff --git a/server/sonar-web/src/main/js/apps/code/reducers/index.js b/server/sonar-web/src/main/js/apps/code/reducers/index.js index 3a86e706e8d..08b74a6710e 100644 --- a/server/sonar-web/src/main/js/apps/code/reducers/index.js +++ b/server/sonar-web/src/main/js/apps/code/reducers/index.js @@ -1,76 +1,12 @@ import _ from 'underscore'; -import { combineReducers } from 'redux'; -import { BROWSE, RECEIVE_COMPONENTS, SHOW_SOURCE } from '../actions'; +import { INIT, BROWSE, START_FETCHING, STOP_FETCHING } from '../actions'; -export function fetching (state = false, action) { - switch (action.type) { - case BROWSE: - return true; - case RECEIVE_COMPONENTS: - return false; - default: - return state; - } -} - - -export function baseComponent (state = null, action) { - switch (action.type) { - case RECEIVE_COMPONENTS: - return action.baseComponent; - default: - return state; - } -} - - -export function components (state = null, action) { - switch (action.type) { - case RECEIVE_COMPONENTS: - return action.components; - default: - return state; - } +function hasSourceCode (component) { + return component.qualifier === 'FIL' || component.qualifier === 'UTS'; } - -export function breadcrumbs (state = [], action) { - switch (action.type) { - case BROWSE: - const existedIndex = state.findIndex(b => b.key === action.baseComponent.key); - let nextBreadcrumbs; - - if (existedIndex === -1) { - // browse deeper - nextBreadcrumbs = [...state, action.baseComponent]; - } else { - // use breadcrumbs - nextBreadcrumbs = [...state.slice(0, existedIndex + 1)]; - } - - return nextBreadcrumbs; - case SHOW_SOURCE: - return [...state, action.component]; - default: - return state; - } -} - - -export function sourceViewer (state = null, action) { - switch (action.type) { - case BROWSE: - return null; - case SHOW_SOURCE: - return action.component; - default: - return state; - } -} - - function selectCoverageMetric (component) { const coverage = _.findWhere(component.msr, { key: 'coverage' }); const itCoverage = _.findWhere(component.msr, { key: 'it_coverage' }); @@ -85,25 +21,66 @@ function selectCoverageMetric (component) { } } +function merge (components, candidate) { + const found = _.findWhere(components, { key: candidate.key }); + const newEntry = Object.assign({}, found, candidate); + return [...(_.without(components, found)), newEntry]; +} -export function coverageMetric (state = null, action) { + +export const initialState = { + fetching: false, + baseComponent: null, + components: null, + breadcrumbs: null, + sourceViewer: null, + coverageMetric: null, + baseBreadcrumbs: [] +}; + + +export function current (state = initialState, action) { switch (action.type) { + case INIT: + const coverageMetric = selectCoverageMetric(action.component); + const baseBreadcrumbs = action.breadcrumbs.length > 1 ? _.initial(action.breadcrumbs) : []; + + return { ...state, coverageMetric, baseBreadcrumbs }; case BROWSE: - return state !== null ? state : selectCoverageMetric(action.baseComponent); + const baseComponent = hasSourceCode(action.component) ? null : action.component; + const components = hasSourceCode(action.component) ? null : _.sortBy(action.children, 'name'); + const baseBreadcrumbsLength = state.baseBreadcrumbs.length; + const breadcrumbs = action.breadcrumbs.slice(baseBreadcrumbsLength); + const sourceViewer = hasSourceCode(action.component) ? action.component : null; + + return { ...state, baseComponent, components, breadcrumbs, sourceViewer }; + case START_FETCHING: + return { ...state, fetching: true }; + case STOP_FETCHING: + return { ...state, fetching: false }; default: return state; } } -const rootReducer = combineReducers({ - fetching, - baseComponent, - components, - breadcrumbs, - sourceViewer, - coverageMetric -}); - - -export default rootReducer; +export function bucket (state = [], action) { + switch (action.type) { + case INIT: + return merge(state, action.component); + case BROWSE: + const candidate = Object.assign({}, action.component, { + children: action.children, + breadcrumbs: action.breadcrumbs + }); + const nextState = merge(state, candidate); + return action.children.reduce((currentState, nextComponent) => { + const nextComponentWidthBreadcrumbs = Object.assign({}, nextComponent, { + breadcrumbs: [...action.breadcrumbs, nextComponent] + }); + return merge(currentState, nextComponentWidthBreadcrumbs); + }, nextState); + default: + return state; + } +} diff --git a/server/sonar-web/src/main/js/apps/code/store/configureStore.js b/server/sonar-web/src/main/js/apps/code/store/configureStore.js index 821c23f5ae0..4b9f858a893 100644 --- a/server/sonar-web/src/main/js/apps/code/store/configureStore.js +++ b/server/sonar-web/src/main/js/apps/code/store/configureStore.js @@ -1,11 +1,11 @@ -import { createStore, applyMiddleware } from 'redux'; +import { createStore, applyMiddleware, combineReducers } from 'redux'; import thunk from 'redux-thunk'; import createLogger from 'redux-logger'; - -import rootReducer from '../reducers'; +import { routeReducer } from 'redux-simple-router'; +import { current, bucket } from '../reducers'; const logger = createLogger({ - predicate: () => process.env.NODE_ENV === 'development' + predicate: () => process.env.NODE_ENV !== 'production' }); const createStoreWithMiddleware = applyMiddleware( @@ -13,7 +13,12 @@ const createStoreWithMiddleware = applyMiddleware( logger )(createStore); +const reducer = combineReducers({ + routing: routeReducer, + current, + bucket +}); export default function configureStore () { - return createStoreWithMiddleware(rootReducer); + return createStoreWithMiddleware(reducer); } diff --git a/server/sonar-web/src/main/js/main/app.js b/server/sonar-web/src/main/js/main/app.js index d902b888ea3..ab58da869ab 100644 --- a/server/sonar-web/src/main/js/main/app.js +++ b/server/sonar-web/src/main/js/main/app.js @@ -37,7 +37,8 @@ function prepareAppOptions (navResponse) { id: navResponse.component.uuid, key: navResponse.component.key, name: navResponse.component.name, - qualifier: _.last(navResponse.component.breadcrumbs).qualifier + qualifier: _.last(navResponse.component.breadcrumbs).qualifier, + breadcrumbs: navResponse.component.breadcrumbs }; } } diff --git a/server/sonar-web/tests/apps/code/components-test.js b/server/sonar-web/tests/apps/code/components-test.js index 5048b2fa8bc..7639d3716f8 100644 --- a/server/sonar-web/tests/apps/code/components-test.js +++ b/server/sonar-web/tests/apps/code/components-test.js @@ -26,13 +26,14 @@ const measures = [ { key: 'ncloc', val: 9757 } ]; const exampleComponent = { + uuid: 'A1', key: 'A', name: 'AA', qualifier: 'TRK', msr: measures }; -const exampleComponent2 = { key: 'B' }; -const exampleComponent3 = { key: 'C' }; +const exampleComponent2 = { uuid: 'B2', key: 'B' }; +const exampleComponent3 = { uuid: 'C3', key: 'C' }; const exampleOnBrowse = sinon.spy(); diff --git a/server/sonar-web/tests/apps/code/store-test.js b/server/sonar-web/tests/apps/code/store-test.js index cf8532fe755..9d3445cf0af 100644 --- a/server/sonar-web/tests/apps/code/store-test.js +++ b/server/sonar-web/tests/apps/code/store-test.js @@ -1,106 +1,119 @@ import { expect } from 'chai'; +import { current, bucket, initialState } from '../../../src/main/js/apps/code/reducers'; import { - fetching, - baseComponent, - components, - breadcrumbs, - sourceViewer, - coverageMetric -} from '../../../src/main/js/apps/code/reducers'; -import { - requestComponents, - receiveComponents, - showSource + initComponentAction, + browseAction, + startFetching, + stopFetching } from '../../../src/main/js/apps/code/actions'; const exampleComponent = { key: 'A' }; +const exampleComponents = [ + { key: 'B' }, + { key: 'C' } +]; describe('Code :: Store', () => { //describe('action creators'); describe('reducers', () => { - describe('fetching', () => { - it('should be initially false', () => { - expect(fetching(undefined, {})) - .to.equal(false); - }); - - it('should be true after requesting components', () => { - expect(fetching(false, requestComponents())) - .to.equal(true); - }); - - it('should be false after receiving components', () => { - expect(fetching(true, receiveComponents({}, []))) - .to.equal(false); - }); - }); - - describe('baseComponent', () => { - it('should not be set after requesting components', () => { - const component = {}; - expect(baseComponent(null, requestComponents(component))) - .to.equal(null); - }); - - it('should be set after receiving components', () => { - const component = {}; - expect(baseComponent(null, receiveComponents(component, []))) - .to.equal(component); - }); - }); + describe('current', () => { + describe('fetching', () => { + it('should be set to true', () => { + expect(current({ ...initialState, fetching: false }, startFetching()).fetching) + .to.equal(true); + }); - describe('components', () => { - it('should be set after receiving components', () => { - const list = [exampleComponent]; - expect(components(null, receiveComponents({}, list))) - .to.equal(list); + it('should be false', () => { + expect(current({ ...initialState, fetching: true }, stopFetching()).fetching) + .to.equal(false); + }); }); - }); + describe('baseComponent', () => { + it('should be set', () => { + const component = {}; + expect(current(initialState, browseAction(component)).baseComponent) + .to.equal(component); + }); - describe('breadcrumbs', () => { - it('should push new component on BROWSE', () => { - const stateBefore = []; - const stateAfter = [exampleComponent]; - expect(breadcrumbs(stateBefore, requestComponents(exampleComponent))) - .to.deep.equal(stateAfter); + it('should not be set for components with source code', () => { + const file = { qualifier: 'FIL' }; + expect(current(initialState, browseAction(file, exampleComponents)).baseComponent) + .to.be.null; + const test = { qualifier: 'UTS' }; + expect(current(initialState, browseAction(test, exampleComponents)).baseComponent) + .to.be.null; + }); }); + describe('components', () => { + it('should be set', () => { + const component = {}; + expect(current(initialState, browseAction(component, exampleComponents)).components) + .to.deep.equal(exampleComponents); + }); - it('should push new component on SHOW_SOURCE', () => { - const stateBefore = []; - const stateAfter = [exampleComponent]; - expect(breadcrumbs(stateBefore, showSource(exampleComponent))) - .to.deep.equal(stateAfter); - }); + it('should sort components by name', () => { + const component = {}; + const componentsBefore = [ + { key: 'A', name: 'B' }, + { key: 'B', name: 'A' } + ]; + const componentsAfter = [ + { key: 'B', name: 'A' }, + { key: 'A', name: 'B' } + ]; + expect(current(initialState, browseAction(component, componentsBefore)).components) + .to.deep.equal(componentsAfter); + }); - it('should cut the tail', () => { - const stateBefore = [{ key: 'B' }, exampleComponent, { key: 'C' }]; - const stateAfter = [{ key: 'B' }, exampleComponent]; - expect(breadcrumbs(stateBefore, requestComponents(exampleComponent))) - .to.deep.equal(stateAfter); + it('should not be set for components with source code', () => { + const file = { qualifier: 'FIL' }; + expect(current(initialState, browseAction(file, exampleComponents)).components) + .to.be.null; + const test = { qualifier: 'UTS' }; + expect(current(initialState, browseAction(test, exampleComponents)).components) + .to.be.null; + }); }); - }); + describe('breadcrumbs', () => { + it('should be set', () => { + expect(current(initialState, browseAction(exampleComponent, [], exampleComponents)).breadcrumbs) + .to.deep.equal(exampleComponents); + }); - describe('sourceViewer', () => { - it('should be set on SHOW_SOURCE', () => { - expect(sourceViewer(null, showSource(exampleComponent))) - .to.equal(exampleComponent); + it('should respect baseBreadcrumbs', () => { + const baseBreadcrumbs = [{ key: 'BASE1' }]; + const breadcrumbsBefore = [{ key: 'BASE1' }, { key: 'BASE2' }, { key: 'C' }]; + const breadcrumbsAfter = [{ key: 'BASE2' }, { key: 'C' }]; + expect(current( + { ...initialState, baseBreadcrumbs }, + browseAction(exampleComponent, [], breadcrumbsBefore)).breadcrumbs + ).to.deep.equal(breadcrumbsAfter); + }); }); + describe('sourceViewer', () => { + it('should be set for components with source code', () => { + const file = { qualifier: 'FIL' }; + expect(current(initialState, browseAction(file, exampleComponents)).sourceViewer) + .to.equal(file); + const test = { qualifier: 'UTS' }; + expect(current(initialState, browseAction(test, exampleComponents)).sourceViewer) + .to.equal(test); + }); - it('should be unset on BROWSE', () => { - expect(sourceViewer(exampleComponent, requestComponents({}))) - .to.equal(null); + it('should not be set for components without source code', () => { + const project = { qualifier: 'TRK' }; + expect(current(initialState, browseAction(project, exampleComponents)).sourceViewer) + .to.be.null; + const unknown = {}; + expect(current(initialState, browseAction(unknown, exampleComponents)).sourceViewer) + .to.be.null; + }); }); - describe('coverageMetric', () => { - it('should be initially null', () => { - expect(coverageMetric(undefined, {})) - .to.equal(null); - }); - it('should be set to "coverage"', () => { const componentWithCoverage = { ...exampleComponent, @@ -109,7 +122,7 @@ describe('Code :: Store', () => { ] }; - expect(coverageMetric(null, requestComponents(componentWithCoverage))) + expect(current(initialState, initComponentAction(componentWithCoverage)).coverageMetric) .to.equal('coverage'); }); @@ -121,7 +134,7 @@ describe('Code :: Store', () => { ] }; - expect(coverageMetric(null, requestComponents(componentWithCoverage))) + expect(current(initialState, initComponentAction(componentWithCoverage)).coverageMetric) .to.equal('it_coverage'); }); @@ -135,7 +148,7 @@ describe('Code :: Store', () => { ] }; - expect(coverageMetric(null, requestComponents(componentWithCoverage))) + expect(current(initialState, initComponentAction(componentWithCoverage)).coverageMetric) .to.equal('overall_coverage'); }); @@ -145,22 +158,106 @@ describe('Code :: Store', () => { msr: [] }; - expect(coverageMetric(null, requestComponents(componentWithCoverage))) + expect(current(initialState, initComponentAction(componentWithCoverage)).coverageMetric) .to.equal('it_coverage'); }); + }); + describe('baseBreadcrumbs', () => { + it('should be empty', () => { + const component = { key: 'A' }; + const breadcrumbs = [{ key: 'A' }]; - it('should not be reset after set once', () => { - const componentWithCoverage = { - ...exampleComponent, - msr: [ - { key: 'coverage', val: 13 } - ] - }; + expect(current(initialState, initComponentAction(component, breadcrumbs)).baseBreadcrumbs) + .to.have.length(0); + }); - expect(coverageMetric('overall_coverage', requestComponents(componentWithCoverage))) - .to.equal('overall_coverage'); + it('should set baseBreadcrumbs', () => { + const component = { key: 'A' }; + const breadcrumbs = [{ key: 'BASE' }, { key: 'A' }]; + + expect(current(initialState, initComponentAction(component, breadcrumbs)).baseBreadcrumbs) + .to.have.length(1); }); }); }); + describe('bucket', () => { + it('should add initial component', () => { + expect(bucket([], initComponentAction(exampleComponent))) + .to.deep.equal([exampleComponent]); + }); + + it('should add browsed component', () => { + const componentBefore = { key: 'A' }; + const childrenBefore = [{ key: 'B' }]; + const breadcrumbsBefore = [{ key: 'A' }]; + + const bucketAfter = [ + { key: 'A', breadcrumbs: [{ key: 'A' }], children: [{ key: 'B' }] }, + { key: 'B', breadcrumbs: [{ key: 'A' }, { key: 'B' }] } + ]; + + expect(bucket([], browseAction(componentBefore, childrenBefore, breadcrumbsBefore))) + .to.deep.equal(bucketAfter); + }); + + it('should merge new components', () => { + const componentBefore = { key: 'A' }; + const childrenBefore = [{ key: 'B' }]; + const breadcrumbsBefore = [{ key: 'A' }]; + + const bucketBefore = [ + { key: 'A' }, + { key: 'B' } + ]; + + const bucketAfter = [ + { + key: 'A', + breadcrumbs: [{ key: 'A' }], + children: [{ key: 'B' }] + }, + { + key: 'B', + breadcrumbs: [{ key: 'A' }, { key: 'B' }] + } + ]; + + expect(bucket(bucketBefore, browseAction(componentBefore, childrenBefore, breadcrumbsBefore))) + .to.deep.equal(bucketAfter); + }); + + it('should work twice in a row', () => { + const componentA = { key: 'A' }; + const childrenA = [{ key: 'B' }]; + const breadcrumbsA = [{ key: 'A' }]; + + const componentB = { key: 'B' }; + const childrenB = [{ key: 'C' }]; + const breadcrumbsB = [{ key: 'A' }, { key: 'B' }]; + + const bucketAfter = [ + { + key: 'A', + breadcrumbs: [{ key: 'A' }], + children: [{ key: 'B' }] + }, + { + key: 'B', + breadcrumbs: [{ key: 'A' }, { key: 'B' }], + children: [{ key: 'C' }] + }, + { + key: 'C', + breadcrumbs: [{ key: 'A' }, { key: 'B' }, { key: 'C' }] + } + ]; + + const afterFirstPass = bucket([], browseAction(componentA, childrenA, breadcrumbsA)); + const afterSecondPass = bucket(afterFirstPass, browseAction(componentB, childrenB, breadcrumbsB)); + + expect(afterSecondPass) + .to.deep.equal(bucketAfter); + }); + }); }); }); -- 2.39.5