Address some security vulnerabilities in frontend (#2435)

Signed-off-by: Cintia Sanchez Garcia <cynthiasg@icloud.com>
This commit is contained in:
Cintia Sánchez García 2022-10-20 10:40:16 +02:00 committed by GitHub
parent 6587198222
commit ae3cf31700
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 194 additions and 91 deletions

2
.github/codeql/codeql-config.yml vendored Normal file
View File

@ -0,0 +1,2 @@
paths-ignore:
- docs/api/*.js

View File

@ -22,6 +22,7 @@ jobs:
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
config-file: ./.github/codeql/codeql-config.yml
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v2 uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis

View File

@ -7,7 +7,7 @@ import useSystemThemeMode from '../hooks/useSystemThemeMode';
import { Prefs, Profile, ThemePrefs, UserFullName } from '../types'; import { Prefs, Profile, ThemePrefs, UserFullName } from '../types';
import cleanLoginUrlParams from '../utils/cleanLoginUrlParams'; import cleanLoginUrlParams from '../utils/cleanLoginUrlParams';
import detectActiveThemeMode from '../utils/detectActiveThemeMode'; import detectActiveThemeMode from '../utils/detectActiveThemeMode';
import history from '../utils/history'; import browserHistory from '../utils/history';
import isControlPanelSectionAvailable from '../utils/isControlPanelSectionAvailable'; import isControlPanelSectionAvailable from '../utils/isControlPanelSectionAvailable';
import lsPreferences from '../utils/localStoragePreferences'; import lsPreferences from '../utils/localStoragePreferences';
import lsStorage from '../utils/localStoragePreferences'; import lsStorage from '../utils/localStoragePreferences';
@ -92,16 +92,20 @@ export async function refreshUserProfile(dispatch: Dispatch<any>, redirectUrl?:
}`; }`;
if (!isUndefined(redirectUrl)) { if (!isUndefined(redirectUrl)) {
if (redirectUrl === currentUrl) { if (redirectUrl === currentUrl) {
history.replace(redirectUrl); browserHistory.replace(redirectUrl);
} else { } else {
const redirection = redirectUrl.split('?');
// Redirect to correct route when necessary // Redirect to correct route when necessary
history.push(redirectUrl); browserHistory.push({
pathname: redirection[0],
search: !isUndefined(redirection[1]) ? `?${redirection[1]}` : '',
});
} }
} }
} catch (err: any) { } catch (err: any) {
dispatch({ type: 'signOut' }); dispatch({ type: 'signOut' });
if (err.message === 'invalid session') { if (err.message === 'invalid session') {
history.push( browserHistory.push(
`${window.location.pathname}${ `${window.location.pathname}${
window.location.search === '' ? '?' : `${window.location.search}&` window.location.search === '' ? '?' : `${window.location.search}&`
}modal=login&redirect=${encodeURIComponent(`${window.location.pathname}${window.location.search}`)}` }modal=login&redirect=${encodeURIComponent(`${window.location.pathname}${window.location.search}`)}`
@ -111,10 +115,10 @@ export async function refreshUserProfile(dispatch: Dispatch<any>, redirectUrl?:
} }
function redirectToControlPanel(context: 'user' | 'org') { function redirectToControlPanel(context: 'user' | 'org') {
if (history.location.pathname.startsWith('/control-panel')) { if (browserHistory.location.pathname.startsWith('/control-panel')) {
const sections = history.location.pathname.split('/'); const sections = browserHistory.location.pathname.split('/');
if (!isControlPanelSectionAvailable(context, sections[2], sections[3])) { if (!isControlPanelSectionAvailable(context, sections[2], sections[3])) {
history.push('/control-panel/repositories'); browserHistory.push('/control-panel/repositories');
} }
} }
} }

View File

@ -7,7 +7,7 @@ import { Route, Router, Switch } from 'react-router-dom';
import { AppCtxProvider } from '../context/AppCtx'; import { AppCtxProvider } from '../context/AppCtx';
import buildSearchParams from '../utils/buildSearchParams'; import buildSearchParams from '../utils/buildSearchParams';
import history from '../utils/history'; import browserHistory from '../utils/history';
import AlertController from './common/AlertController'; import AlertController from './common/AlertController';
import UserNotificationsController from './common/userNotifications'; import UserNotificationsController from './common/userNotifications';
import ControlPanelView from './controlPanel'; import ControlPanelView from './controlPanel';
@ -38,7 +38,7 @@ export default function App() {
return ( return (
<AppCtxProvider> <AppCtxProvider>
<Router history={history}> <Router history={browserHistory}>
<div className="d-flex flex-column min-vh-100 position-relative whiteBranded"> <div className="d-flex flex-column min-vh-100 position-relative whiteBranded">
<div className="visually-hidden visually-hidden-focusable"> <div className="visually-hidden visually-hidden-focusable">
<a href="#content">Skip to Main Content</a> <a href="#content">Skip to Main Content</a>
@ -59,29 +59,37 @@ export default function App() {
'/delete-user', '/delete-user',
]} ]}
exact exact
render={({ location }) => ( render={(routeProps) => (
<div className="d-flex flex-column flex-grow-1"> <div className="d-flex flex-column flex-grow-1">
<Navbar <Navbar
isSearching={isSearching} isSearching={isSearching}
redirect={getQueryParam(location.search, 'redirect') || undefined} redirect={getQueryParam(routeProps.location.search, 'redirect') || undefined}
visibleModal={getQueryParam(location.search, 'modal') || undefined} visibleModal={getQueryParam(routeProps.location.search, 'modal') || undefined}
fromHome fromHome
/> />
<HomeView <HomeView
isSearching={isSearching} isSearching={isSearching}
emailCode={ emailCode={
location.pathname === '/verify-email' ? getQueryParam(location.search, 'code') : undefined routeProps.location.pathname === '/verify-email'
? getQueryParam(routeProps.location.search, 'code')
: undefined
} }
deleteCode={ deleteCode={
location.pathname === '/delete-user' ? getQueryParam(location.search, 'code') : undefined routeProps.location.pathname === '/delete-user'
? getQueryParam(routeProps.location.search, 'code')
: undefined
} }
resetPwdCode={ resetPwdCode={
location.pathname === '/reset-password' ? getQueryParam(location.search, 'code') : undefined routeProps.location.pathname === '/reset-password'
? getQueryParam(routeProps.location.search, 'code')
: undefined
} }
orgToConfirm={ orgToConfirm={
location.pathname === '/accept-invitation' ? getQueryParam(location.search, 'org') : undefined routeProps.location.pathname === '/accept-invitation'
? getQueryParam(routeProps.location.search, 'org')
: undefined
} }
onOauthFailed={location.pathname === '/oauth-failed'} onOauthFailed={routeProps.location.pathname === '/oauth-failed'}
/> />
<Footer /> <Footer />
</div> </div>
@ -91,13 +99,13 @@ export default function App() {
<Route <Route
path="/packages/search" path="/packages/search"
exact exact
render={({ location }: any) => { render={(routeProps: any) => {
const searchParams = buildSearchParams(location.search); const searchParams = buildSearchParams(routeProps.location.search);
return ( return (
<> <>
<Navbar <Navbar
redirect={getQueryParam(location.search, 'redirect') || undefined} redirect={getQueryParam(routeProps.location.search, 'redirect') || undefined}
visibleModal={getQueryParam(location.search, 'modal') || undefined} visibleModal={getQueryParam(routeProps.location.search, 'modal') || undefined}
isSearching={isSearching} isSearching={isSearching}
searchText={searchParams.tsQueryWeb} searchText={searchParams.tsQueryWeb}
/> />
@ -108,8 +116,10 @@ export default function App() {
setIsSearching={setIsSearching} setIsSearching={setIsSearching}
scrollPosition={scrollPosition} scrollPosition={scrollPosition}
setScrollPosition={setScrollPosition} setScrollPosition={setScrollPosition}
fromDetail={location.state ? location.state.hasOwnProperty('from-detail') : false} fromDetail={
visibleModal={getQueryParam(location.search, 'modal') || undefined} routeProps.location.state ? routeProps.location.state.hasOwnProperty('from-detail') : false
}
visibleModal={getQueryParam(routeProps.location.search, 'modal') || undefined}
/> />
</div> </div>
</> </>
@ -120,32 +130,32 @@ export default function App() {
<Route <Route
path="/packages/:repositoryKind/:repositoryName/:packageName/:version?" path="/packages/:repositoryKind/:repositoryName/:packageName/:version?"
exact exact
render={({ location, match }) => { render={(routeProps) => {
const state = location.state ? (location.state as Object) : {}; const state = routeProps.location.state ? (routeProps.location.state as Object) : {};
return ( return (
<> <>
<Navbar <Navbar
isSearching={isSearching} isSearching={isSearching}
redirect={getQueryParam(location.search, 'redirect') || undefined} redirect={getQueryParam(routeProps.location.search, 'redirect') || undefined}
visibleModal={getQueryParam(location.search, 'modal') || undefined} visibleModal={getQueryParam(routeProps.location.search, 'modal') || undefined}
/> />
<div className="d-flex flex-column flex-grow-1"> <div className="d-flex flex-column flex-grow-1">
<PackageView <PackageView
hash={location.hash} hash={routeProps.location.hash}
visibleModal={getQueryParam(location.search, 'modal') || undefined} visibleModal={getQueryParam(routeProps.location.search, 'modal') || undefined}
visibleTemplate={getQueryParam(location.search, 'template') || undefined} visibleTemplate={getQueryParam(routeProps.location.search, 'template') || undefined}
visibleLine={getQueryParam(location.search, 'line') || undefined} visibleLine={getQueryParam(routeProps.location.search, 'line') || undefined}
compareVersionTo={getQueryParam(location.search, 'compare-to') || undefined} compareVersionTo={getQueryParam(routeProps.location.search, 'compare-to') || undefined}
visibleExample={getQueryParam(location.search, 'example') || undefined} visibleExample={getQueryParam(routeProps.location.search, 'example') || undefined}
visibleFile={getQueryParam(location.search, 'file') || undefined} visibleFile={getQueryParam(routeProps.location.search, 'file') || undefined}
visibleVersion={getQueryParam(location.search, 'version') || undefined} visibleVersion={getQueryParam(routeProps.location.search, 'version') || undefined}
visibleValuesPath={getQueryParam(location.search, 'path') || undefined} visibleValuesPath={getQueryParam(routeProps.location.search, 'path') || undefined}
visibleImage={getQueryParam(location.search, 'image') || undefined} visibleImage={getQueryParam(routeProps.location.search, 'image') || undefined}
visibleTarget={getQueryParam(location.search, 'target') || undefined} visibleTarget={getQueryParam(routeProps.location.search, 'target') || undefined}
visibleSection={getQueryParam(location.search, 'section') || undefined} visibleSection={getQueryParam(routeProps.location.search, 'section') || undefined}
eventId={getQueryParam(location.search, 'event-id') || undefined} eventId={getQueryParam(routeProps.location.search, 'event-id') || undefined}
{...state} {...state}
{...match.params} {...routeProps.match.params}
/> />
</div> </div>
</> </>
@ -156,17 +166,17 @@ export default function App() {
<Route <Route
path="/control-panel/:section?/:subsection?" path="/control-panel/:section?/:subsection?"
exact exact
render={({ location, match }) => ( render={(routeProps) => (
<> <>
<Navbar isSearching={isSearching} privateRoute /> <Navbar isSearching={isSearching} privateRoute />
<div className="d-flex flex-column flex-grow-1"> <div className="d-flex flex-column flex-grow-1">
<ControlPanelView <ControlPanelView
{...match.params} {...routeProps.match.params}
visibleModal={getQueryParam(location.search, 'modal') || undefined} visibleModal={getQueryParam(routeProps.location.search, 'modal') || undefined}
userAlias={getQueryParam(location.search, 'user-alias') || undefined} userAlias={getQueryParam(routeProps.location.search, 'user-alias') || undefined}
organizationName={getQueryParam(location.search, 'org-name') || undefined} organizationName={getQueryParam(routeProps.location.search, 'org-name') || undefined}
repoName={getQueryParam(location.search, 'repo-name') || undefined} repoName={getQueryParam(routeProps.location.search, 'repo-name') || undefined}
activePage={getQueryParam(location.search, 'page') || undefined} activePage={getQueryParam(routeProps.location.search, 'page') || undefined}
/> />
</div> </div>
<Footer /> <Footer />
@ -177,11 +187,11 @@ export default function App() {
<Route <Route
path="/packages/starred" path="/packages/starred"
exact exact
render={({ location }) => ( render={(routeProps) => (
<> <>
<Navbar isSearching={isSearching} privateRoute /> <Navbar isSearching={isSearching} privateRoute />
<div className="d-flex flex-column flex-grow-1"> <div className="d-flex flex-column flex-grow-1">
<StarredPackagesView activePage={getQueryParam(location.search, 'page') || undefined} /> <StarredPackagesView activePage={getQueryParam(routeProps.location.search, 'page') || undefined} />
</div> </div>
<Footer /> <Footer />
</> </>
@ -191,15 +201,15 @@ export default function App() {
<Route <Route
path="/stats" path="/stats"
exact exact
render={({ location }) => ( render={(routeProps) => (
<> <>
<Navbar <Navbar
isSearching={isSearching} isSearching={isSearching}
redirect={getQueryParam(location.search, 'redirect') || undefined} redirect={getQueryParam(routeProps.location.search, 'redirect') || undefined}
visibleModal={getQueryParam(location.search, 'modal') || undefined} visibleModal={getQueryParam(routeProps.location.search, 'modal') || undefined}
/> />
<div className="d-flex flex-column flex-grow-1"> <div className="d-flex flex-column flex-grow-1">
<StatsView hash={location.hash} /> <StatsView hash={routeProps.location.hash} />
</div> </div>
<Footer /> <Footer />
</> </>

View File

@ -1,5 +1,6 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { BrowserRouter as Router } from 'react-router-dom';
import AnchorHeader from './AnchorHeader'; import AnchorHeader from './AnchorHeader';
@ -41,19 +42,31 @@ describe('AnchorHeader', () => {
}); });
it('creates snapshot', () => { it('creates snapshot', () => {
const { asFragment } = render(<AnchorHeader {...defaultProps} />); const { asFragment } = render(
<Router>
<AnchorHeader {...defaultProps} />
</Router>
);
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });
it('renders header properly', () => { it('renders header properly', () => {
render(<AnchorHeader {...defaultProps} />); render(
<Router>
<AnchorHeader {...defaultProps} />
</Router>
);
const header = screen.getByText(/Header/i); const header = screen.getByText(/Header/i);
expect(header).toBeInTheDocument(); expect(header).toBeInTheDocument();
expect(header.tagName).toBe('H2'); expect(header.tagName).toBe('H2');
}); });
it('calls scroll into view', async () => { it('calls scroll into view', async () => {
render(<AnchorHeader {...defaultProps} />); render(
<Router>
<AnchorHeader {...defaultProps} />
</Router>
);
const link = screen.getByRole('button'); const link = screen.getByRole('button');
expect(link).toBeInTheDocument(); expect(link).toBeInTheDocument();
await userEvent.click(link); await userEvent.click(link);
@ -62,7 +75,11 @@ describe('AnchorHeader', () => {
for (let i = 0; i < tests.length; i++) { for (let i = 0; i < tests.length; i++) {
it('renders proper id and href from title', () => { it('renders proper id and href from title', () => {
render(<AnchorHeader {...defaultProps} title={tests[i].title} anchorName={tests[i].anchor} />); render(
<Router>
<AnchorHeader {...defaultProps} title={tests[i].title} anchorName={tests[i].anchor} />
</Router>
);
expect(screen.getByText(new RegExp(tests[i].title, 'i'))).toBeInTheDocument(); expect(screen.getByText(new RegExp(tests[i].title, 'i'))).toBeInTheDocument();
const link = screen.getByRole('button'); const link = screen.getByRole('button');
expect(link).toHaveProperty('href', `http://localhost/#${tests[i].id}`); expect(link).toHaveProperty('href', `http://localhost/#${tests[i].id}`);

View File

@ -2,9 +2,10 @@ import { isObject, isString } from 'lodash';
import isUndefined from 'lodash/isUndefined'; import isUndefined from 'lodash/isUndefined';
import { ElementType, MouseEvent as ReactMouseEvent } from 'react'; import { ElementType, MouseEvent as ReactMouseEvent } from 'react';
import { GoLink } from 'react-icons/go'; import { GoLink } from 'react-icons/go';
import { Link } from 'react-router-dom';
import getAnchorValue from '../../utils/getAnchorValue'; import getAnchorValue from '../../utils/getAnchorValue';
import history from '../../utils/history'; import browserHistory from '../../utils/history';
import styles from './AnchorHeader.module.css'; import styles from './AnchorHeader.module.css';
interface Props { interface Props {
level: number; level: number;
@ -39,8 +40,8 @@ const AnchorHeader: ElementType = (props: Props) => {
<span className={styles.header}> <span className={styles.header}>
<Tag className={`position-relative anchorHeader ${styles.headingWrapper} ${props.className}`}> <Tag className={`position-relative anchorHeader ${styles.headingWrapper} ${props.className}`}>
<div data-testid="anchor" className={`position-absolute ${styles.headerAnchor}`} id={anchor} /> <div data-testid="anchor" className={`position-absolute ${styles.headerAnchor}`} id={anchor} />
<a <Link
href={`${history.location.pathname}#${anchor}`} to={{ pathname: browserHistory.location.pathname, hash: anchor }}
onClick={(e: ReactMouseEvent<HTMLAnchorElement, MouseEvent>) => { onClick={(e: ReactMouseEvent<HTMLAnchorElement, MouseEvent>) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -51,7 +52,8 @@ const AnchorHeader: ElementType = (props: Props) => {
aria-label={value} aria-label={value}
> >
<GoLink /> <GoLink />
</a> </Link>
{props.title || props.children} {props.title || props.children}
</Tag> </Tag>
</span> </span>

View File

@ -1,5 +1,6 @@
import { render, screen, waitFor } from '@testing-library/react'; import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { BrowserRouter as Router } from 'react-router-dom';
import TOC from './TOC'; import TOC from './TOC';
jest.mock('react-markdown', () => () => <div>Readme</div>); jest.mock('react-markdown', () => () => <div>Readme</div>);
@ -55,20 +56,32 @@ describe('TOC', () => {
}); });
it('creates snapshot', () => { it('creates snapshot', () => {
const { asFragment } = render(<TOC {...defaultProps} />); const { asFragment } = render(
<Router>
<TOC {...defaultProps} />
</Router>
);
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });
describe('Render', () => { describe('Render', () => {
it('renders properly', () => { it('renders properly', () => {
render(<TOC {...defaultProps} />); render(
<Router>
<TOC {...defaultProps} />
</Router>
);
expect(screen.getByText('Readme')).toBeInTheDocument(); expect(screen.getByText('Readme')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Table of contents/ })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /Table of contents/ })).toBeInTheDocument();
}); });
it('displays dropdown', async () => { it('displays dropdown', async () => {
render(<TOC {...defaultProps} />); render(
<Router>
<TOC {...defaultProps} />
</Router>
);
const btn = screen.getByRole('button', { name: /Table of contents/ }); const btn = screen.getByRole('button', { name: /Table of contents/ });
await userEvent.click(btn); await userEvent.click(btn);
@ -84,7 +97,11 @@ describe('TOC', () => {
}); });
it('displays dropdown', async () => { it('displays dropdown', async () => {
render(<TOC {...defaultProps} />); render(
<Router>
<TOC {...defaultProps} />
</Router>
);
const btn = screen.getByRole('button', { name: /Table of contents/ }); const btn = screen.getByRole('button', { name: /Table of contents/ });
await userEvent.click(btn); await userEvent.click(btn);
@ -97,12 +114,20 @@ describe('TOC', () => {
}); });
it('renders support button', () => { it('renders support button', () => {
render(<TOC {...defaultProps} supportLink="http://link.test" />); render(
<Router>
<TOC {...defaultProps} supportLink="http://link.test" />
</Router>
);
expect(screen.getByRole('button', { name: 'Open support link' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Open support link' })).toBeInTheDocument();
}); });
it('does not render component when list is empty', () => { it('does not render component when list is empty', () => {
const { container } = render(<TOC {...defaultProps} toc={[]} />); const { container } = render(
<Router>
<TOC {...defaultProps} toc={[]} />
</Router>
);
expect(container).toBeEmptyDOMElement(); expect(container).toBeEmptyDOMElement();
}); });
}); });

View File

@ -1,5 +1,6 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { BrowserRouter as Router } from 'react-router-dom';
import TOCEntry from './TOCEntry'; import TOCEntry from './TOCEntry';
@ -22,13 +23,21 @@ describe('TOCEntry', () => {
}); });
it('creates snapshot', () => { it('creates snapshot', () => {
const { asFragment } = render(<TOCEntry {...defaultProps} />); const { asFragment } = render(
<Router>
<TOCEntry {...defaultProps} />
</Router>
);
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });
describe('Render', () => { describe('Render', () => {
it('renders properly', () => { it('renders properly', () => {
render(<TOCEntry {...defaultProps} />); render(
<Router>
<TOCEntry {...defaultProps} />
</Router>
);
const link = screen.getByText('Installing the Chart'); const link = screen.getByText('Installing the Chart');
expect(link).toBeInTheDocument(); expect(link).toBeInTheDocument();
@ -37,7 +46,11 @@ describe('TOCEntry', () => {
}); });
it('clicks link', async () => { it('clicks link', async () => {
render(<TOCEntry {...defaultProps} />); render(
<Router>
<TOCEntry {...defaultProps} />
</Router>
);
const link = screen.getByText('Installing the Chart'); const link = screen.getByText('Installing the Chart');
await userEvent.click(link); await userEvent.click(link);
@ -51,7 +64,11 @@ describe('TOCEntry', () => {
describe('does not render element', () => { describe('does not render element', () => {
it('when value is an empty string', () => { it('when value is an empty string', () => {
const { container } = render(<TOCEntry {...defaultProps} entry={{ ...defaultProps.entry, value: '' }} />); const { container } = render(
<Router>
<TOCEntry {...defaultProps} entry={{ ...defaultProps.entry, value: '' }} />
</Router>
);
expect(container).toBeEmptyDOMElement(); expect(container).toBeEmptyDOMElement();
}); });
}); });

View File

@ -1,9 +1,10 @@
import { Dispatch, MouseEvent as ReactMouseEvent, SetStateAction } from 'react'; import { Dispatch, MouseEvent as ReactMouseEvent, SetStateAction } from 'react';
import { Link } from 'react-router-dom';
import { TOCEntryItem } from '../../../types'; import { TOCEntryItem } from '../../../types';
import cleanTOCEntry from '../../../utils/cleanTOCEntry'; import cleanTOCEntry from '../../../utils/cleanTOCEntry';
import getAnchorValue from '../../../utils/getAnchorValue'; import getAnchorValue from '../../../utils/getAnchorValue';
import history from '../../../utils/history'; import browserHistory from '../../../utils/history';
import styles from './TOCEntry.module.css'; import styles from './TOCEntry.module.css';
interface Props { interface Props {
@ -21,11 +22,11 @@ const TOCEntry = (props: Props) => {
return ( return (
<div className={`${styles.dropdownItem} dropdownItem`}> <div className={`${styles.dropdownItem} dropdownItem`}>
<a <Link
to={{ pathname: browserHistory.location.pathname, hash: link }}
className={`btn btn-link d-inline-block w-100 text-decoration-none ms-0 text-muted text-start ${styles.btn} ${ className={`btn btn-link d-inline-block w-100 text-decoration-none ms-0 text-muted text-start ${styles.btn} ${
styles[`level${props.level}`] styles[`level${props.level}`]
}`} }`}
href={`${history.location.pathname}#${link}`}
onClick={(e: ReactMouseEvent<HTMLAnchorElement, MouseEvent>) => { onClick={(e: ReactMouseEvent<HTMLAnchorElement, MouseEvent>) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -38,7 +39,7 @@ const TOCEntry = (props: Props) => {
aria-selected={false} aria-selected={false}
> >
{title} {title}
</a> </Link>
</div> </div>
); );
}; };

View File

@ -1,4 +1,5 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom';
import TOCList from './TOCList'; import TOCList from './TOCList';
@ -53,13 +54,21 @@ describe('TOCList', () => {
}); });
it('creates snapshot', () => { it('creates snapshot', () => {
const { asFragment } = render(<TOCList {...defaultProps} />); const { asFragment } = render(
<Router>
<TOCList {...defaultProps} />
</Router>
);
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });
describe('Render', () => { describe('Render', () => {
it('renders properly', () => { it('renders properly', () => {
render(<TOCList {...defaultProps} />); render(
<Router>
<TOCList {...defaultProps} />
</Router>
);
expect(screen.getByText('Title 1')).toBeInTheDocument(); expect(screen.getByText('Title 1')).toBeInTheDocument();
expect(screen.getByText('Subtitle 1a')).toBeInTheDocument(); expect(screen.getByText('Subtitle 1a')).toBeInTheDocument();

View File

@ -1,6 +1,7 @@
import { render, screen, waitFor } from '@testing-library/react'; import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { mocked } from 'jest-mock'; import { mocked } from 'jest-mock';
import { BrowserRouter as Router } from 'react-router-dom';
import API from '../../api'; import API from '../../api';
import { AppCtx } from '../../context/AppCtx'; import { AppCtx } from '../../context/AppCtx';
@ -55,7 +56,9 @@ describe('StatsView', () => {
const { asFragment } = render( const { asFragment } = render(
<AppCtx.Provider value={{ ctx: mockCtx, dispatch: jest.fn() }}> <AppCtx.Provider value={{ ctx: mockCtx, dispatch: jest.fn() }}>
<StatsView /> <Router>
<StatsView />
</Router>
</AppCtx.Provider> </AppCtx.Provider>
); );
@ -76,7 +79,9 @@ describe('StatsView', () => {
render( render(
<AppCtx.Provider value={{ ctx: mockCtx, dispatch: jest.fn() }}> <AppCtx.Provider value={{ ctx: mockCtx, dispatch: jest.fn() }}>
<StatsView /> <Router>
<StatsView />
</Router>
</AppCtx.Provider> </AppCtx.Provider>
); );
@ -100,7 +105,9 @@ describe('StatsView', () => {
render( render(
<AppCtx.Provider value={{ ctx: mockCtx, dispatch: jest.fn() }}> <AppCtx.Provider value={{ ctx: mockCtx, dispatch: jest.fn() }}>
<StatsView /> <Router>
<StatsView />
</Router>
</AppCtx.Provider> </AppCtx.Provider>
); );
@ -123,7 +130,9 @@ describe('StatsView', () => {
render( render(
<AppCtx.Provider value={{ ctx: mockCtx, dispatch: jest.fn() }}> <AppCtx.Provider value={{ ctx: mockCtx, dispatch: jest.fn() }}>
<StatsView /> <Router>
<StatsView />
</Router>
</AppCtx.Provider> </AppCtx.Provider>
); );
@ -137,7 +146,9 @@ describe('StatsView', () => {
render( render(
<AppCtx.Provider value={{ ctx: mockCtx, dispatch: jest.fn() }}> <AppCtx.Provider value={{ ctx: mockCtx, dispatch: jest.fn() }}>
<StatsView /> <Router>
<StatsView />
</Router>
</AppCtx.Provider> </AppCtx.Provider>
); );
@ -154,7 +165,9 @@ describe('StatsView', () => {
render( render(
<AppCtx.Provider value={{ ctx: mockCtx, dispatch: jest.fn() }}> <AppCtx.Provider value={{ ctx: mockCtx, dispatch: jest.fn() }}>
<StatsView /> <Router>
<StatsView />
</Router>
</AppCtx.Provider> </AppCtx.Provider>
); );
@ -174,7 +187,9 @@ describe('StatsView', () => {
render( render(
<AppCtx.Provider value={{ ctx: mockCtx, dispatch: jest.fn() }}> <AppCtx.Provider value={{ ctx: mockCtx, dispatch: jest.fn() }}>
<StatsView hash="#repositories" /> <Router>
<StatsView hash="#repositories" />
</Router>
</AppCtx.Provider> </AppCtx.Provider>
); );

View File

@ -1,4 +1,4 @@
import history from './history'; import browserHistory from './history';
jest.mock('./updateMetaIndex', () => jest.fn()); jest.mock('./updateMetaIndex', () => jest.fn());
@ -19,14 +19,14 @@ describe('history', () => {
}); });
it('calls analytics and update meta', () => { it('calls analytics and update meta', () => {
history.push('/new-page'); browserHistory.push('/new-page');
expect(mockAnalytics.page).toHaveBeenCalledTimes(1); expect(mockAnalytics.page).toHaveBeenCalledTimes(1);
expect(mockAnalytics.page).toHaveBeenCalledWith(); expect(mockAnalytics.page).toHaveBeenCalledWith();
expect(mockUpdateMeta).toHaveBeenCalledTimes(1); expect(mockUpdateMeta).toHaveBeenCalledTimes(1);
}); });
it('calls analytics, but not update meta when pathname starts with /packages/(helm|falco|opa|olm|tbaction|krew|helm-plugin|tekton-task)/', () => { it('calls analytics, but not update meta when pathname starts with /packages/(helm|falco|opa|olm|tbaction|krew|helm-plugin|tekton-task)/', () => {
history.push('/packages/helm/123'); browserHistory.push('/packages/helm/123');
expect(mockAnalytics.page).toHaveBeenCalledTimes(1); expect(mockAnalytics.page).toHaveBeenCalledTimes(1);
expect(mockAnalytics.page).toHaveBeenCalledWith(); expect(mockAnalytics.page).toHaveBeenCalledWith();
expect(mockUpdateMeta).toHaveBeenCalledTimes(0); expect(mockUpdateMeta).toHaveBeenCalledTimes(0);

View File

@ -6,7 +6,7 @@ import getMetaTag from './getMetaTag';
import updateMetaIndex from './updateMetaIndex'; import updateMetaIndex from './updateMetaIndex';
import notificationsDispatcher from './userNotificationsDispatcher'; import notificationsDispatcher from './userNotificationsDispatcher';
const history = createBrowserHistory(); const browserHistory = createBrowserHistory();
const analyticsConfig: string | null = getMetaTag('gaTrackingID'); const analyticsConfig: string | null = getMetaTag('gaTrackingID');
let analytics: any = null; let analytics: any = null;
@ -17,7 +17,7 @@ if (!isNull(analyticsConfig)) {
}); });
} }
history.listen((location) => { browserHistory.listen((location) => {
// Updates meta tags every time that history is called for all locations except for package detail page // Updates meta tags every time that history is called for all locations except for package detail page
if (!PKG_DETAIL_PATH.test(location.pathname)) { if (!PKG_DETAIL_PATH.test(location.pathname)) {
updateMetaIndex(); updateMetaIndex();
@ -31,4 +31,4 @@ history.listen((location) => {
notificationsDispatcher.dismissNotification(location.pathname); notificationsDispatcher.dismissNotification(location.pathname);
}); });
export default history; export default browserHistory;