mirror of https://github.com/artifacthub/hub.git
Address some security vulnerabilities in frontend (#2435)
Signed-off-by: Cintia Sanchez Garcia <cynthiasg@icloud.com>
This commit is contained in:
parent
6587198222
commit
ae3cf31700
|
|
@ -0,0 +1,2 @@
|
||||||
|
paths-ignore:
|
||||||
|
- docs/api/*.js
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 />
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue