Unverified Commit de1964a5 authored by Manoj Pandian Sakthivel's avatar Manoj Pandian Sakthivel Committed by GitHub
Browse files

Browser nav bar state update (#477)

parent eefd53bb
Pipeline #139702 passed with stages
in 5 minutes and 33 seconds
......@@ -16,35 +16,41 @@
import React from 'react';
import { MemoryRouter } from 'react-router';
import { mount } from 'enzyme';
import { render } from '@testing-library/react';
import faker from 'faker';
import { BreakpointWidth } from 'src/global/globalConfig';
import { Browser, BrowserProps, ExampleObjectLinks } from './Browser';
import BrowserImage from './browser-image/BrowserImage';
import TrackPanel from './track-panel/TrackPanel';
import BrowserNavBar from './browser-nav/BrowserNavBar';
import { Browser, BrowserProps } from './Browser';
import { createChrLocationValues } from 'tests/fixtures/browser';
jest.mock('./hooks/useBrowserRouting', () => () => ({
changeGenomeId: jest.fn()
}));
jest.mock('./browser-bar/BrowserBar', () => () => <div>BrowserBar</div>);
jest.mock('./browser-image/BrowserImage', () => () => <div>BrowserImage</div>);
jest.mock('./browser-nav/BrowserNavBar', () => () => <div>BrowserNavBar</div>);
jest.mock('./track-panel/TrackPanel', () => () => <div>TrackPanel</div>);
jest.mock('./browser-bar/BrowserBar', () => () => (
<div className="browserBar">BrowserBar</div>
));
jest.mock('./browser-image/BrowserImage', () => () => (
<div className="browserImage">BrowserImage</div>
));
jest.mock('./browser-nav/BrowserNavBar', () => () => (
<div className="browserNavBar">BrowserNavBar</div>
));
jest.mock('./track-panel/TrackPanel', () => () => (
<div className="trackPanel">TrackPanel</div>
));
jest.mock('./browser-app-bar/BrowserAppBar', () => () => (
<div>BrowserAppBar</div>
<div className="browserAppBar">BrowserAppBar</div>
));
jest.mock('./track-panel/track-panel-bar/TrackPanelBar', () => () => (
<div>TrackPanelBar</div>
<div className="trackPanelBar">TrackPanelBar</div>
));
jest.mock('./track-panel/track-panel-tabs/TrackPanelTabs', () => () => (
<div>TrackPanelTabs</div>
<div className="trackPanelTabs">TrackPanelTabs</div>
));
jest.mock('./drawer/Drawer', () => () => <div>Drawer</div>);
jest.mock('./drawer/Drawer', () => () => <div className="drawer">Drawer</div>);
jest.mock('ensembl-genome-browser', () => {
return;
});
......@@ -54,7 +60,7 @@ const defaultProps: BrowserProps = {
activeGenomeId: faker.lorem.words(),
activeEnsObjectId: faker.lorem.words(),
browserActivated: false,
browserNavOpened: false,
browserNavOpenState: false,
browserQueryParams: {},
chrLocation: createChrLocationValues().tupleValue,
isDrawerOpened: false,
......@@ -70,43 +76,51 @@ describe('<Browser />', () => {
jest.resetAllMocks();
});
const wrappingComponent = (props: any) => (
<MemoryRouter>{props.children}</MemoryRouter>
);
const mountBrowserComponent = (props?: Partial<BrowserProps>) =>
mount(<Browser {...defaultProps} {...props} />, { wrappingComponent });
render(
<MemoryRouter>
<Browser {...defaultProps} {...props} />
</MemoryRouter>
);
describe('rendering', () => {
test('does not render when no activeGenomeId', () => {
const wrapper = mountBrowserComponent({ activeGenomeId: null });
expect(wrapper.html()).toBe(null);
const { container } = mountBrowserComponent({ activeGenomeId: null });
expect(container.innerHTML).toBeFalsy();
});
test('renders links to example objects only if there is no selected focus feature', () => {
const wrapper = mountBrowserComponent();
expect(wrapper.find(ExampleObjectLinks)).toHaveLength(1);
const { container, rerender } = mountBrowserComponent();
wrapper.setProps({
browserQueryParams: {
focus: faker.lorem.words()
}
});
expect(wrapper.find(ExampleObjectLinks)).toHaveLength(0);
expect(container.querySelectorAll('.exampleLinks')).toHaveLength(1);
rerender(
<Browser
{...defaultProps}
browserQueryParams={{
focus: faker.lorem.words()
}}
/>
);
expect(container.querySelectorAll('.exampleLinks')).toHaveLength(0);
});
test('renders the genome browser and track panel only when there is a selected focus feature', () => {
const wrapper = mountBrowserComponent();
const { container, rerender } = mountBrowserComponent();
expect(wrapper.find(BrowserImage)).toHaveLength(0);
expect(wrapper.find(TrackPanel)).toHaveLength(0);
expect(container.querySelectorAll('.browserImage')).toHaveLength(0);
expect(container.querySelectorAll('.trackPanel')).toHaveLength(0);
wrapper.setProps({
browserQueryParams: { focus: faker.lorem.words() }
});
rerender(
<Browser
{...defaultProps}
browserQueryParams={{ focus: faker.lorem.words() }}
/>
);
expect(wrapper.find(BrowserImage)).toHaveLength(1);
expect(wrapper.find(TrackPanel)).toHaveLength(1);
expect(container.querySelectorAll('.browserImage')).toHaveLength(1);
expect(container.querySelectorAll('.trackPanel')).toHaveLength(1);
});
describe('BrowserNavBar', () => {
......@@ -116,23 +130,25 @@ describe('<Browser />', () => {
browserQueryParams: { focus: 'foo' }
};
it('is rendered when props.browserNavOpened is true', () => {
const wrapper = mountBrowserComponent(props);
expect(wrapper.find(BrowserNavBar).length).toBe(0);
it('is rendered when props.browserNavOpenState is true', () => {
const { container, rerender } = mountBrowserComponent(props);
expect(container.querySelectorAll('.browserNavBar')).toHaveLength(0);
wrapper.setProps({ browserNavOpened: true });
expect(wrapper.find(BrowserNavBar).length).toBe(1);
rerender(<Browser {...props} browserNavOpenState={true} />);
expect(container.querySelectorAll('.browserNavBar')).toHaveLength(1);
});
it('is not rendered if drawer is opened', () => {
const wrapper = mountBrowserComponent({
const { container, rerender } = mountBrowserComponent({
...props,
browserNavOpened: true
browserNavOpenState: true
});
expect(wrapper.find(BrowserNavBar).length).toBe(1);
wrapper.setProps({ isDrawerOpened: true });
expect(wrapper.find(BrowserNavBar).length).toBe(0);
expect(container.querySelectorAll('.browserNavBar')).toHaveLength(1);
rerender(<Browser {...defaultProps} isDrawerOpened={true} />);
expect(container.querySelectorAll('.browserNavBar')).toHaveLength(0);
});
});
});
......
......@@ -35,7 +35,7 @@ import { toggleTrackPanel } from 'src/content/app/browser/track-panel/trackPanel
import { toggleDrawer } from './drawer/drawerActions';
import {
getBrowserNavOpened,
getBrowserNavOpenState,
getChrLocation,
getBrowserActivated,
getBrowserActiveGenomeId,
......@@ -71,7 +71,7 @@ export type BrowserProps = {
activeGenomeId: string | null;
activeEnsObjectId: string | null;
browserActivated: boolean;
browserNavOpened: boolean;
browserNavOpenState: boolean;
browserQueryParams: { [key: string]: string };
chrLocation: ChrLocation | null;
isDrawerOpened: boolean;
......@@ -105,7 +105,7 @@ export const Browser = (props: BrowserProps) => {
};
const shouldShowNavBar =
props.browserActivated && props.browserNavOpened && !isDrawerOpened;
props.browserActivated && props.browserNavOpenState && !isDrawerOpened;
if (!props.activeGenomeId) {
return null;
......@@ -183,7 +183,7 @@ const mapStateToProps = (state: RootState) => {
activeEnsObjectId: getBrowserActiveEnsObjectId(state),
allActiveEnsObjectIds: getBrowserActiveEnsObjectIds(state),
browserActivated: getBrowserActivated(state),
browserNavOpened: getBrowserNavOpened(state),
browserNavOpenState: getBrowserNavOpenState(state),
browserQueryParams: getBrowserQueryParams(state),
chrLocation: getChrLocation(state),
isDrawerOpened: getIsDrawerOpened(state),
......
......@@ -42,11 +42,12 @@ describe('<BrowserImage />', () => {
const defaultProps: BrowserImageProps = {
browserCogTrackList: {},
browserNavOpened: false,
isNavbarOpen: false,
activeGenomeId: '',
isDisabled: false,
browserActivated: false,
activateBrowser: jest.fn(),
updateBrowserNavStates: jest.fn(),
updateBrowserNavIconStates: jest.fn(),
updateBrowserActivated: jest.fn(),
updateBrowserActiveEnsObject: jest.fn(),
setChrLocation: jest.fn(),
......
......@@ -29,15 +29,16 @@ import { parseFeatureId } from 'src/content/app/browser/browserHelper';
import { buildEnsObjectId } from 'src/shared/state/ens-object/ensObjectHelpers';
import {
getBrowserCogTrackList,
getBrowserNavOpened,
getBrowserNavOpenState,
getBrowserActivated,
getRegionEditorActive,
getRegionFieldActive
getRegionFieldActive,
getBrowserActiveGenomeId
} from '../browserSelectors';
import {
activateBrowser,
updateBrowserActivated,
updateBrowserNavStates,
updateBrowserNavIconStates,
setChrLocation,
setActualChrLocation,
updateBrowserActiveEnsObjectIdsAndSave,
......@@ -46,7 +47,12 @@ import {
import { changeHighlightedTrackId } from 'src/content/app/browser/track-panel/trackPanelActions';
import { BrowserNavStates, ChrLocation, CogList } from '../browserState';
import {
BrowserNavAction,
BrowserNavIconStates,
ChrLocation,
CogList
} from '../browserState';
import { RootState } from 'src/store';
import { BROWSER_CONTAINER_ID } from '../browser-constants';
......@@ -54,11 +60,15 @@ import styles from './BrowserImage.scss';
export type BrowserImageProps = {
browserCogTrackList: CogList;
browserNavOpened: boolean;
isNavbarOpen: boolean;
browserActivated: boolean;
isDisabled: boolean;
activeGenomeId: string | null;
activateBrowser: () => void;
updateBrowserNavStates: (browserNavStates: BrowserNavStates) => void;
updateBrowserNavIconStates: (payload: {
activeGenomeId: string;
navStates: BrowserNavIconStates;
}) => void;
updateBrowserActivated: (browserActivated: boolean) => void;
updateBrowserActiveEnsObject: (objectId: string) => void;
setChrLocation: (chrLocation: ChrLocation) => void;
......@@ -67,8 +77,17 @@ export type BrowserImageProps = {
changeHighlightedTrackId: (trackId: string) => void;
};
export type BumperPayload = [
top: boolean,
right: boolean,
bottom: boolean,
left: boolean,
zoomOut: boolean,
zoomIn: boolean
];
type BpaneOutPayload = {
bumper?: BrowserNavStates;
bumper?: BumperPayload;
focus?: string;
'message-counter'?: number;
'intended-location'?: ChrLocation;
......@@ -87,13 +106,26 @@ export const BrowserImage = (props: BrowserImageProps) => {
const browserRef = useRef<HTMLDivElement>(null);
const listenBpaneOut = useCallback((payload: BpaneOutPayload) => {
const ensObjectId = payload.focus;
const navIconStates = payload.bumper as BrowserNavStates;
const intendedLocation = payload['intended-location'];
const actualLocation = payload['actual-location'] || intendedLocation;
const isFocusObjectInDefaultPosition = payload['is-focus-position'];
if (navIconStates) {
props.updateBrowserNavStates(navIconStates);
if (payload.bumper && props.activeGenomeId) {
// Invert the flags to make it appropriate for the react side
const navIconStates = payload.bumper.map((a) => !a);
const navStates = {
[BrowserNavAction.NAVIGATE_UP]: navIconStates[0],
[BrowserNavAction.NAVIGATE_DOWN]: navIconStates[1],
[BrowserNavAction.ZOOM_OUT]: navIconStates[2],
[BrowserNavAction.ZOOM_IN]: navIconStates[3],
[BrowserNavAction.NAVIGATE_LEFT]: navIconStates[4],
[BrowserNavAction.NAVIGATE_RIGHT]: navIconStates[5]
};
props.updateBrowserNavIconStates({
activeGenomeId: props.activeGenomeId,
navStates
});
}
if (intendedLocation) {
......@@ -134,7 +166,7 @@ export const BrowserImage = (props: BrowserImageProps) => {
}, []);
const browserContainerClassNames = classNames(styles.browserStage, {
[styles.shorter]: props.browserNavOpened
[styles.shorter]: props.isNavbarOpen
});
return (
......@@ -160,15 +192,16 @@ export const BrowserImage = (props: BrowserImageProps) => {
const mapStateToProps = (state: RootState) => ({
browserCogTrackList: getBrowserCogTrackList(state),
browserNavOpened: getBrowserNavOpened(state),
isNavbarOpen: getBrowserNavOpenState(state),
browserActivated: getBrowserActivated(state),
activeGenomeId: getBrowserActiveGenomeId(state),
isDisabled: getRegionEditorActive(state) || getRegionFieldActive(state)
});
const mapDispatchToProps = {
activateBrowser,
updateBrowserActivated,
updateBrowserNavStates,
updateBrowserNavIconStates,
updateBrowserActiveEnsObject: updateBrowserActiveEnsObjectIdsAndSave,
setChrLocation,
setActualChrLocation,
......
......@@ -15,28 +15,43 @@
*/
import React from 'react';
import { render } from '@testing-library/react';
import { screen, render } from '@testing-library/react';
import faker from 'faker';
import times from 'lodash/times';
import { BrowserNavBarControls } from './BrowserNavBarControls';
import { BrowserNavStates } from '../browserState';
import { BrowserNavAction, BrowserNavIconStates } from '../browserState';
import { BrowserNavItem } from 'src/content/app/browser/browserConfig';
jest.mock(
'./BrowserNavIcon',
() => (props: { enabled: boolean; browserNavItem: BrowserNavItem }) => {
const className = props.enabled
? 'browserNavIcon enabled'
: 'browserNavIcon';
return (
<div className={className} data-test-id={props.browserNavItem.name} />
);
}
);
jest.mock('./BrowserNavIcon', () => (props: { enabled: boolean }) => {
const className = props.enabled ? 'browserNavIcon enabled' : 'browserNavIcon';
return <div className={className} />;
});
jest.mock('src/shared/components/overlay/Overlay', () => () => (
<div className="overlay" />
));
const browserNavStates = times(6, faker.random.boolean) as BrowserNavStates;
const browserNavIconStates: BrowserNavIconStates = {
[BrowserNavAction.NAVIGATE_UP]: faker.random.boolean(),
[BrowserNavAction.NAVIGATE_RIGHT]: faker.random.boolean(),
[BrowserNavAction.NAVIGATE_DOWN]: faker.random.boolean(),
[BrowserNavAction.NAVIGATE_LEFT]: faker.random.boolean(),
[BrowserNavAction.ZOOM_OUT]: faker.random.boolean(),
[BrowserNavAction.ZOOM_IN]: faker.random.boolean()
};
describe('BrowserNavBarControls', () => {
it('has an overlay on top when browser nav bar controls are disabled', () => {
const { container } = render(
<BrowserNavBarControls
browserNavStates={browserNavStates}
browserNavIconStates={browserNavIconStates}
isDisabled={true}
/>
);
......@@ -44,20 +59,18 @@ describe('BrowserNavBarControls', () => {
});
it('disables buttons if corresponding actions are not possible', () => {
// browserNavStates are an array of booleans that indicate whether the button
// has already caused maximum corresponding effect, and will have no further effect if pressed
const { container } = render(
render(
<BrowserNavBarControls
browserNavStates={browserNavStates}
browserNavIconStates={browserNavIconStates}
isDisabled={false}
/>
);
const controlButtons = container.querySelectorAll('.browserNavIcon');
controlButtons.forEach((button, index) => {
const isDisabled = browserNavStates[index];
expect(button.classList.contains('enabled')).toBe(!isDisabled);
Object.keys(browserNavIconStates).forEach((icon) => {
const navIcon = screen.getByTestId(icon);
expect(navIcon.classList.contains('enabled')).toBe(
browserNavIconStates[icon as BrowserNavAction]
);
});
});
});
......@@ -22,28 +22,28 @@ import Overlay from 'src/shared/components/overlay/Overlay';
import { browserNavConfig, BrowserNavItem } from '../browserConfig';
import {
getBrowserNavStates,
getBrowserNavIconStates,
getRegionEditorActive,
getRegionFieldActive
} from '../browserSelectors';
import { BrowserNavStates } from '../browserState';
import { BrowserNavIconStates } from '../browserState';
import { RootState } from 'src/store';
import styles from './BrowserNavBarControls.scss';
type Props = {
browserNavStates: BrowserNavStates;
browserNavIconStates: BrowserNavIconStates;
isDisabled: boolean;
};
export const BrowserNavBarControls = (props: Props) => (
<div className={styles.browserNavBarControls}>
{browserNavConfig.map((item: BrowserNavItem, index: number) => (
{browserNavConfig.map((item: BrowserNavItem) => (
<BrowserNavIcon
key={item.name}
browserNavItem={item}
enabled={!props.browserNavStates[index]}
enabled={props.browserNavIconStates[item.name]}
/>
))}
{props.isDisabled && <Overlay className={styles.overlay} />}
......@@ -55,7 +55,7 @@ const mapStateToProps = (state: RootState) => {
getRegionEditorActive(state) || getRegionFieldActive(state);
return {
browserNavStates: getBrowserNavStates(state),
browserNavIconStates: getBrowserNavIconStates(state),
isDisabled
};
};
......
......@@ -22,7 +22,7 @@ import { BrowserNavIcon } from './BrowserNavIcon';
import browserMessagingService from 'src/content/app/browser/browser-messaging-service';
import { browserNavConfig } from '../browserConfig';
describe('<BrowserNavIcon />', () => {
describe('<BrowserNavAction />', () => {
const browserNavItem = browserNavConfig[0];
test('sends navigation message when clicked', () => {
......
......@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, { FunctionComponent, memo } from 'react';
import React, { memo } from 'react';
import browserMessagingService from 'src/content/app/browser/browser-messaging-service';
......@@ -30,9 +30,7 @@ type BrowserNavIconProps = {
enabled: boolean;
};
export const BrowserNavIcon: FunctionComponent<BrowserNavIconProps> = (
props: BrowserNavIconProps
) => {
export const BrowserNavIcon = (props: BrowserNavIconProps) => {
const { browserNavItem, enabled } = props;
const { detail, icon } = browserNavItem;
......
......@@ -38,7 +38,7 @@ import {
getBrowserTrackStates,
getChrLocation,
getBrowserActiveEnsObjectIds,
getBrowserNavOpened
getBrowserNavOpenState
} from './browserSelectors';
import { updatePreviouslyViewedObjectsAndSave } from 'src/content/app/browser/track-panel/trackPanelActions';
......@@ -51,7 +51,7 @@ import {
import { BROWSER_CONTAINER_ID } from './browser-constants';
import {
BrowserNavStates,
BrowserNavIconStates,
ChrLocation,
CogList,
ChrLocations
......@@ -224,18 +224,22 @@ export const restoreBrowserTrackStates: ActionCreator<ThunkAction<
export const openBrowserNav = createAction(
'browser/open-browser-navigation',
() => {
(activeGenomeId: string) => {
analyticsTracking.trackEvent({
category: 'browser_navigation',
label: 'open_browser_navigation',
action: 'clicked'
});
return {
activeGenomeId
};
}
)();
)<{ activeGenomeId: string }>();
export const closeBrowserNav = createAction(
'browser/close-browser-navigation'
)();
)<{ activeGenomeId: string }>();
export const toggleBrowserNav: ActionCreator<ThunkAction<
any,
......@@ -244,19 +248,24 @@ export const toggleBrowserNav: ActionCreator<ThunkAction<
Action<string>
>> = () => {
return (dispatch: Dispatch, getState: () => RootState) => {
const isBrowserNavOpened = getBrowserNavOpened(getState());
const state = getState();
const isBrowserNavOpenState = getBrowserNavOpenState(state);
const activeGenomeId = getBrowserActiveGenomeId(state);
if (isBrowserNavOpened) {
dispatch(closeBrowserNav());
if (!activeGenomeId) {
return;
}
if (isBrowserNavOpenState) {
dispatch(closeBrowserNav({ activeGenomeId }));
} else {
dispatch(openBrowserNav());
dispatch(openBrowserNav(activeGenomeId));
}
};
};
export const updateBrowserNavStates = createAction(