Commit 8726903a authored by Mahdi Mahmoudy's avatar Mahdi Mahmoudy
Browse files

merge with 2020/04 sprint branch

parents bde054f6 ed630eeb
Pipeline #75168 passed with stages
in 4 minutes and 37 seconds
......@@ -11,10 +11,6 @@ before_script:
before_deploy:
- yarn build-staging
notifications:
email:
- xwatkins@ebi.ac.uk
- mmahmoudy@ebi.ac.uk
- dlrice@ebi.ac.uk
slack: ebi-uniprot:7OMFmmkUp9uxH3SXgyq7OVb3
deploy:
provider: pages
......
......@@ -42,6 +42,9 @@
"^react$": "<rootDir>/node_modules/react/",
"^react-router-dom$": "<rootDir>/node_modules/react-router-dom/"
},
"globals": {
"BASE_URL": "/"
},
"snapshotSerializers": [
"<rootDir>/jest.serializer.js"
]
......@@ -55,6 +58,7 @@
"d3": "5.16.0",
"d3-scale": "3.2.1",
"franklin-sites": "0.0.44",
"history": "4.10.1",
"idx": "2.5.6",
"interaction-viewer": "2.7.0",
"lit-html": "1.2.1",
......@@ -77,6 +81,7 @@
"react-redux": "7.2.0",
"react-router-dom": "5.1.2",
"react-router-hash-link": "1.2.2",
"react-spring": "8.0.27",
"redux": "4.0.5",
"redux-persist": "6.0.0",
"redux-persist-transform-filter": "0.0.20",
......@@ -90,13 +95,14 @@
"@babel/core": "7.9.0",
"@babel/plugin-proposal-class-properties": "7.8.3",
"@babel/plugin-proposal-object-rest-spread": "7.9.5",
"@babel/plugin-transform-runtime": "^7.9.0",
"@babel/plugin-transform-runtime": "7.9.0",
"@babel/preset-env": "7.9.5",
"@babel/preset-react": "7.9.4",
"@babel/preset-typescript": "7.9.0",
"@svgr/webpack": "5.4.0",
"@testing-library/jest-dom": "5.5.0",
"@testing-library/react": "10.0.3",
"@testing-library/react-hooks": "3.2.1",
"@types/d3": "5.7.2",
"@types/jest": "25.2.1",
"@types/node": "13.13.4",
......@@ -111,7 +117,7 @@
"@typescript-eslint/parser": "2.30.0",
"axios-mock-adapter": "1.18.1",
"babel-core": "7.0.0-bridge.0",
"babel-jest": "25.4.0",
"babel-jest": "25.5.1",
"babel-loader": "8.1.0",
"css-loader": "3.5.3",
"eslint": "6.8.0",
......@@ -122,8 +128,8 @@
"eslint-plugin-react": "7.19.0",
"eslint-plugin-react-hooks": "3.0.0",
"html-loader": "1.1.0",
"html-webpack-plugin": "4.2.0",
"jest": "25.4.0",
"html-webpack-plugin": "4.2.1",
"jest": "25.5.1",
"jest-css-modules-transform": "4.0.0",
"node-sass": "4.14.0",
"path": "0.12.7",
......@@ -131,7 +137,7 @@
"react-test-renderer": "16.13.1",
"redux-mock-store": "1.5.4",
"sass-loader": "8.0.2",
"style-loader": "1.2.0",
"style-loader": "1.2.1",
"svg-inline-loader": "0.8.2",
"ts-jest": "25.4.0",
"typescript": "3.8.3",
......
import React, { lazy, Suspense } from 'react';
import {
BrowserRouter as Router,
Router,
Route,
Switch,
Redirect,
......@@ -8,9 +8,9 @@ import {
import { FranklinSite, Loader } from 'franklin-sites';
import * as Sentry from '@sentry/browser';
import BaseLayout from './layout/BaseLayout';
import { Location, LocationToPath } from './urls';
import './styles/App.scss';
declare const BASE_URL: string;
import history from './utils/browserHistory';
if (process.env.NODE_ENV !== 'development') {
Sentry.init({
......@@ -25,22 +25,37 @@ const EntryPage = lazy(() => import('./pages/EntryPage'));
const CustomiseTablePage = lazy(() => import('./pages/CustomiseTablePage'));
const DownloadPage = lazy(() => import('./pages/DownloadPage'));
const ContactPage = lazy(() => import('./pages/ContactPage'));
// export const queryBuilderPath = '/advancedSearch';
const ResourceNotFoundPage = lazy(() =>
import('./pages/errors/ResourceNotFoundPage')
);
const ServiceUnavailablePage = lazy(() =>
import('./pages/errors/ServiceUnavailablePage')
);
const JobErrorPage = lazy(() => import('./pages/errors/JobErrorPage'));
const App = () => (
<FranklinSite>
<Router basename={BASE_URL}>
<Router history={history}>
<Suspense fallback={<Loader />}>
<Switch>
<Route path="/" exact>
<Redirect to="/uniprotkb?query=*" />
<Route
path="/" exact>
<Redirect to={LocationToPath[Location.ShowAllResults]} />
</Route>
<Route path="/uniprotkb/:accession" render={() => <EntryPage />} />
<Route path="/uniprotkb" render={() => <ResultsPage />} />
<Route path="/contact" render={() => <ContactPage />} />
<Route
path={LocationToPath[Location.Contact]}
render={() => <ContactPage />}
/>
<Route
path={LocationToPath[Location.UniProtKBEntry]}
render={() => <EntryPage />}
/>
<Route
path={LocationToPath[Location.UniProtKBResults]}
render={() => <ResultsPage />}
/>
<Route
path="/customise-table"
path={LocationToPath[Location.UniProtKBCustomiseTable]}
render={() => (
<BaseLayout>
<CustomiseTablePage />
......@@ -48,15 +63,39 @@ const App = () => (
)}
/>
<Route
path="/download"
path={LocationToPath[Location.UniProtKBDownload]}
render={() => (
<BaseLayout>
<DownloadPage />
</BaseLayout>
)}
/>
<Route
path={LocationToPath[Location.PageNotFound]}
render={() => (
<BaseLayout>
<ResourceNotFoundPage />
</BaseLayout>
)}
/>
<Route
path={LocationToPath[Location.ServiceUnavailable]}
render={() => (
<BaseLayout>
<ServiceUnavailablePage />
</BaseLayout>
)}
/>
<Route
path={LocationToPath[Location.JobError]}
render={() => (
<BaseLayout>
<JobErrorPage />
</BaseLayout>
)}
/>
{/* <Route
path={`${queryBuilderPath}(/reset)?`}
path={`${LocationToPath[Location.UniProtKBQueryBuilder]}(/reset)?`}
render={() => (
<BaseLayout isSearchPage>
<AdvancedSearchPage />
......
{
"facets": [{
"label": "Status",
"name": "reviewed",
"allowMultipleSelection": false
}, {
"label": "Popular organisms",
"name": "popular_organism",
"allowMultipleSelection": true
}, {
"label": "Proteins with",
"name": "proteins_with",
"allowMultipleSelection": true
}, {
"label": "Protein Existence",
"name": "existence",
"allowMultipleSelection": true
}, {
"label": "Annotation Score",
"name": "annotation_score",
"allowMultipleSelection": true
}, {
"label": "Sequence length",
"name": "length",
"allowMultipleSelection": true,
"values": [{
"label": "1 - 200",
"value": "[1 TO 200]",
"count": 0
}, {
"label": "201 - 400",
"value": "[201 TO 400]",
"count": 0
}, {
"label": "401 - 600",
"value": "[401 TO 600]",
"count": 0
}, {
"label": "601 - 800",
"value": "[601 TO 800]",
"count": 0
}, {
"label": ">= 801",
"value": "[801 TO *]",
"count": 0
}]
}],
"matchedFields": [],
"results": []
}
\ No newline at end of file
......@@ -6,11 +6,12 @@ import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import { createMemoryHistory, MemoryHistory } from 'history';
import rootReducer from '../state/rootReducer';
import { Router } from 'react-router-dom';
import { Router, Route } from 'react-router-dom';
type RenderOptions = {
route?: string;
history?: MemoryHistory<any>;
path?: string;
initialState?: any;
store?: any;
};
......@@ -20,6 +21,7 @@ const renderWithRedux = (
{
route = '',
history = createMemoryHistory({ initialEntries: [route] }),
path,
initialState,
store = createStore(rootReducer, initialState, applyMiddleware(thunk)),
}: RenderOptions = {}
......@@ -27,7 +29,9 @@ const renderWithRedux = (
return {
...render(
<Provider store={store}>
<Router history={history}>{ui}</Router>
<Router history={history}>
{path ? <Route path={path} render={() => ui} /> : ui}
</Router>
</Provider>
),
store,
......
import React, { FC, useState } from 'react';
import './styles/gdpr.scss';
import '../styles/gdpr.scss';
const UP_COVID_GDPR = 'UP_COVID_GDPR';
......
import React from 'react';
import { render, fireEvent, waitForElement } from '@testing-library/react';
import GDPR from '../GDPR';
import { local } from 'd3';
describe('GDPR', () => {
const store = {};
beforeEach(() => {
spyOn(localStorage, 'getItem').and.callFake(key => {
store[key];
});
spyOn(localStorage, 'setItem').and.callFake((key, value) => {
store[key] = value;
});
});
test('should render', () => {
const { asFragment } = render(<GDPR />);
expect(asFragment()).toMatchSnapshot();
});
test('should add UP_COVID_GDPR: true to localStorage', () => {
const { getByText } = render(<GDPR />);
const acceptButton = getByText('Accept');
fireEvent.click(acceptButton);
expect(localStorage.getItem('UP_COVID_GDPR')).toBe('true');
});
test('if UP_COVID_GDPR in localStorage, do not render component', () => {
localStorage.setItem('UP_COVID_GDPR', 'true');
const { queryByText } = render(<GDPR />);
const text = queryByText('Privacy Notice');
expect(text).toBeNull();
});
});
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GDPR should render 1`] = `
<DocumentFragment>
<div
class="gdpr-section"
>
We'd like to inform you that we have updated our
<a
href="https://www.uniprot.org/help/privacy"
>
Privacy Notice
</a>
to
comply with Europe’s new General Data Protection Regulation (GDPR) that
applies since 25 May 2018.
<button
class="button secondary"
type="button"
>
Accept
</button>
</div>
</DocumentFragment>
`;
......@@ -20,7 +20,11 @@ import {
import { connect } from 'react-redux';
import { Dispatch, bindActionCreators } from 'redux';
import UniProtKBEntryConfig from '../view/uniprotkb/UniProtEntryConfig';
import { UniProtkbUIModel } from '../model/uniprotkb/UniProtkbConverter';
import {
UniProtkbUIModel,
EntryType,
UniProtkbInactiveEntryModel,
} from '../model/uniprotkb/UniProtkbConverter';
import { hasContent, hasExternalLinks } from '../model/utils/utils';
import EntrySection from '../model/types/EntrySection';
import EntryMain from './EntryMain';
......@@ -39,6 +43,8 @@ import { LiteratureForProteinAPI } from '../literature/types/LiteratureTypes';
import SideBarLayout from '../layout/SideBarLayout';
import { Facet } from '../types/responseTypes';
import submitBlast from '../blast_website/BlastUtils';
import BaseLayout from '../layout/BaseLayout';
import ObsoleteEntryPage from '../pages/errors/ObsoleteEntryPage';
type MatchParams = {
accession: string;
......@@ -46,7 +52,7 @@ type MatchParams = {
};
type EntryProps = RouteComponentProps<MatchParams> & {
entryData: UniProtkbUIModel | null;
entryData: UniProtkbUIModel | UniProtkbInactiveEntryModel | null;
publicationsData: {
data: LiteratureForProteinAPI[] | null;
facets: Facet[];
......@@ -86,7 +92,21 @@ const Entry: React.FC<EntryProps> = ({
return <Loader />;
}
const sections = UniProtKBEntryConfig.map((section) => ({
if (entryData && entryData.entryType === EntryType.INACTIVE) {
const inactiveEntryData : UniProtkbInactiveEntryModel =
entryData as UniProtkbInactiveEntryModel;
return (
<BaseLayout>
<ObsoleteEntryPage
accession={accession}
details={inactiveEntryData.inactiveReason}
/>
</BaseLayout>
);
}
const sections = UniProtKBEntryConfig.map(section => ({
label: section.name,
id: section.name,
......
......@@ -7,6 +7,8 @@ import { fireEvent, waitFor } from '@testing-library/dom';
import Entry from '../Entry';
import renderWithRedux from '../../__testHelpers__/renderWithRedux';
import entryData from '../../model/__mocks__/entryModelData.json';
import deletedEntryData from '../../model/__mocks__/deletedEntryModelData.json';
import demergedEntryData from '../../model/__mocks__/demergedEntryModelData.json';
import entryPublicationsData from '../publications/__mocks__/entryPublicationsData.json';
import entryInitialState from '../state/entryInitialState';
......@@ -16,12 +18,16 @@ import apiUrls, {
} from '../../utils/apiUrls';
const { primaryAccession } = entryData;
const { primaryAccession: deleteEntryAccession } = deletedEntryData;
const { primaryAccession: demergedEntryAccession } = demergedEntryData;
const mock = new MockAdapter(axios);
const filteredUrl = getUniProtPublicationsQueryUrl(primaryAccession, [
{ name: 'scale', value: 'Small' },
]);
mock.onGet(apiUrls.entry(deleteEntryAccession)).reply(200, deletedEntryData);
mock.onGet(apiUrls.entry(demergedEntryAccession)).reply(200, demergedEntryData);
mock.onGet(apiUrls.entry(primaryAccession)).reply(200, entryData);
mock
.onGet(getUniProtPublicationsQueryUrl(primaryAccession, []))
......@@ -87,4 +93,50 @@ describe('Entry', () => {
expect(smallFacetButton2).toBeTruthy();
});
});
it('should render obsolete page for deleted entries', async () => {
component = renderWithRedux(
<Route
component={(props) => <Entry {...props} />}
path="/uniprotkb/:accession"
/>,
{
route: `/uniprotkb/${deleteEntryAccession}`,
initialState: {
entry: {
...entryInitialState,
},
},
}
);
await act(async () => {
const { findByTestId } = component;
const message = await findByTestId('deleted-entry-message');
expect(message).toBeTruthy();
});
});
it('should render obsolete page for demerged entries', async () => {
component = renderWithRedux(
<Route
component={(props) => <Entry {...props} />}
path="/uniprotkb/:accession"
/>,
{
route: `/uniprotkb/${demergedEntryAccession}`,
initialState: {
entry: {
...entryInitialState,
},
},
}
);
await act(async () => {
const { findByTestId } = component;
const message = await findByTestId('demerged-entry-message');
expect(message).toBeTruthy();
});
});
});
......@@ -2045,6 +2045,58 @@ exports[`Entry view should render 1`] = `
id="<UUID>"
/>
</section>
<div
class="loader-container"
>
<svg
class="loader"
height="100"
viewBox="0 0 100 100"
width="100"
>
<g
fill="none"
fill-rule="evenodd"
>
<path
d="M9 50c6.833-10.667 13.667-16 20.5-16S43.167 39.333 50 50s13.667 16 20.5 16S84.167 60.667 91 50"
stroke="#3ca3ca"
stroke-linecap="square"
stroke-width="2"
/>
<circle
cx="9"
cy="50"
fill="#3ca3ca"
r="6"
/>
<circle
cx="30"
cy="34"
fill="#3ca3ca"
r="6"
/>
<circle
cx="50"
cy="50"
fill="#3ca3ca"
r="6"
/>
<circle
cx="70"
cy="66"
fill="#3ca3ca"
r="6"
/>
<circle
cx="91"
cy="50"
fill="#3ca3ca"
r="6"
/>
</g>
</svg>
</div>
</div>
</section>
</div>
......
......@@ -12,6 +12,8 @@ import { LiteratureForProteinAPI } from '../../literature/types/LiteratureTypes'
import getNextUrlFromResponse from '../../utils/queryUtils';
import Response, { Facet } from '../../types/responseTypes';
import { RootState } from '../../state/state-types';
import history from '../../utils/browserHistory';
import { Location, LocationToPath } from '../../urls';
export const REQUEST_ENTRY = 'REQUEST_ENTRY';
export const RECEIVE_ENTRY = 'RECEIVE_ENTRY';
......@@ -51,8 +53,28 @@ export const fetchEntry = (accession: string) => async (dispatch: Dispatch) => {
fetchData(url)
.then(({ data }: { data: UniProtkbAPIModel }) => {
dispatch(receiveEntry(accession, data));
}) /* eslint-disable no-console */
.catch(error => console.error(error));
})
.catch(error => {
const { status } = error.response;
switch (status) {
case 400:
case 404:
history.push(LocationToPath[Location.PageNotFound]);
break;
case 500:
case 503:
history.push(LocationToPath[Location.ServiceUnavailable]);
break;
default:
break;
}
/* eslint-disable no-console */
console.error(error);
});
};
export const fetchEntryIfNeeded = (accession: string) => (
......
import { UniProtkbUIModel } from '../../model/uniprotkb/UniProtkbConverter';
import {
UniProtkbUIModel,
UniProtkbInactiveEntryModel,
} from '../../model/uniprotkb/UniProtkbConverter';
import { LiteratureForProteinAPI } from '../../literature/types/LiteratureTypes';
import { Facet } from '../../types/responseTypes';
export type EntryState = {
accession: string | null;
isFetching: boolean;
data: UniProtkbUIModel | null;
data: UniProtkbUIModel | UniProtkbInactiveEntryModel | null;
publicationsData: {
isFetching: boolean;
data: LiteratureForProteinAPI[];
......
import { renderHook, cleanup } from '@testing-library/react-hooks';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import useDataApi from '../useDataApi';
const url = '/some/path';