fix: refactoring version page / fix issue not found page #100 (#117)

* chore: refactoring version page

* refactor: migrate version page to hooks

* refactor: Version page better imports

* fix: #100 render not found on click item

* test: add test for version page

* chore: update mocks

* test: add scenario for not found package

* chore: fix wrong mock path

* chore: update mock

* chore: add todo list
This commit is contained in:
Juan Picado @jotadeveloper
2019-08-25 14:34:27 +02:00
committed by GitHub
parent e7d3c461cd
commit 97e8448098
42 changed files with 25186 additions and 678 deletions

View File

@@ -0,0 +1,27 @@
import React, { FC, ReactElement } from 'react';
import Grid from '@material-ui/core/Grid';
import DetailContainer from '../../components/DetailContainer';
import DetailSidebar from '../../components/DetailSidebar';
function renderDetail(): ReactElement<HTMLElement> {
return <DetailContainer />;
}
function renderSidebar(): ReactElement<HTMLElement> {
return <DetailSidebar />;
}
const Layout: FC<{}> = () => {
return (
<Grid className={'container content'} container={true} data-testid="version-layout" spacing={0}>
<Grid item={true} xs={8}>
{renderDetail()}
</Grid>
<Grid item={true} xs={4}>
{renderSidebar()}
</Grid>
</Grid>
);
};
export { Layout };

View File

@@ -0,0 +1,96 @@
import React from 'react';
import { render, cleanup } from '@testing-library/react';
import { MemoryRouter } from 'react-router';
import vueMetadata from '../../../test/fixtures/metadata/vue.json';
import Version from './Version';
import { waitForElement } from '@testing-library/dom';
import ErrorBoundary from '../../App/AppError';
import { LABEL_NOT_FOUND } from '../../components/NotFound/NotFound';
// import { NOT_FOUND_TEXT } from '../../components/NotFound/NotFound';
// :-) we mock this otherways fails on render, some weird issue on material-ui
jest.mock('@material-ui/core/Avatar');
describe('test Version page', () => {
beforeAll(() => {
// FIXME: a better way to mock this
// @ts-ignore
global.window.VERDACCIO_API_URL = 'http://test';
});
afterEach(() => {
cleanup();
});
beforeEach(() => {
jest.resetAllMocks();
});
test('should render the version page', async () => {
const readmeText = 'test';
// @ts-ignore
fetch.mockResponses(
[[JSON.stringify(vueMetadata)], { status: 200, headers: { 'content-type': 'application/json' } }],
[[`<p align="center">${readmeText}</p>`], { status: 200, headers: { 'content-type': 'text/html' } }]
);
const { getByTestId, getByText } = render(
<ErrorBoundary>
<MemoryRouter>
<Version match={{ params: { ['package']: 'vue' } }}></Version>
</MemoryRouter>
</ErrorBoundary>
);
// first it display loading
const hasLoading = getByTestId('loading');
expect(hasLoading).toBeTruthy();
// we wait fetch response (mocked above)
await waitForElement(() => getByTestId('version-layout'));
// check whether readme was loaded
const hasReadme = getByText(readmeText);
expect(hasReadme).toBeTruthy();
});
test('should render 404 page if the resources are not found', async () => {
// @ts-ignore
fetch.mockResponses(
[[JSON.stringify({})], { status: 404, headers: { 'content-type': 'application/json' } }],
[[``], { status: 404, headers: { 'content-type': 'text/html' } }]
);
const { getByTestId, getByText } = render(
<ErrorBoundary>
<MemoryRouter>
<Version match={{ params: { ['package']: 'vue' } }}></Version>
</MemoryRouter>
</ErrorBoundary>
);
// first it display loading
const hasLoading = getByTestId('loading');
expect(hasLoading).toBeTruthy();
// we wait fetch response (mocked above)
await waitForElement(() => getByTestId('404'));
// check whether readme was loaded
const hasReadme = getByText(LABEL_NOT_FOUND);
expect(hasReadme).toBeTruthy();
});
// Wanna contribute? Here we some scenarios we need to test
test.todo('should test click on tabs');
test.todo('should check what is rendered int he sidebar is correct');
test.todo('should test click back home on 404');
test.todo('should test click on elements in the sidebar');
test.todo('should test other not consider scenarios');
});

View File

@@ -0,0 +1,69 @@
import React, { useEffect, useState } from 'react';
import { callDetailPage, callReadme } from '../../utils/calls';
import { buildScopePackage } from '../../utils/package';
import Loading from '../../components/Loading/Loading';
import NotFound from '../../components/NotFound';
import { Layout } from './Layout';
import { DetailContextProvider } from './context';
import { StateInterface } from './types';
export function getRouterPackageName(params): string {
const packageName = params.package;
const { scope } = params;
if (scope) {
return buildScopePackage(scope, packageName);
}
return packageName;
}
const Version = ({ match: { params } }) => {
const pkgName = getRouterPackageName(params);
const [readMe, setReadme] = useState();
const [packageName, setPackageName] = useState(pkgName);
const [packageMeta, setPackageMeta] = useState();
const [isLoading, setIsLoading] = useState(true);
const [notFound, setNotFound] = useState(false);
useEffect(() => {
(async () => {
try {
const packageMeta = (await callDetailPage(packageName)) as Partial<StateInterface>;
const readMe = (await callReadme(packageName)) as Partial<StateInterface>;
setReadme(readMe);
setPackageMeta(packageMeta);
setIsLoading(false);
} catch (error) {
setNotFound(true);
setIsLoading(false);
}
})();
}, [packageName]);
useEffect(() => {
document.title = `Verdaccio - ${packageName}`;
}, [packageName]);
useEffect(() => {
const pkgName = getRouterPackageName(params);
setPackageName(pkgName);
}, [params]);
const isNotFound = notFound || typeof packageMeta === 'undefined' || typeof packageName === 'undefined' || typeof readMe === 'undefined';
const renderContent = (): React.ReactElement<HTMLElement> => {
if (isLoading) {
return <Loading />;
} else if (isNotFound) {
return <NotFound />;
} else {
return <Layout />;
}
};
return <DetailContextProvider value={{ packageMeta, readMe, packageName, enableLoading: setIsLoading }}>{renderContent()}</DetailContextProvider>;
};
export default Version;

View File

@@ -0,0 +1,7 @@
import React, { Consumer, Provider } from 'react';
import { DetailContextProps, VersionPageConsumerProps } from './types';
export const DetailContext = React.createContext<Partial<DetailContextProps>>({});
export const DetailContextProvider: Provider<Partial<VersionPageConsumerProps>> = DetailContext.Provider;
export const DetailContextConsumer: Consumer<Partial<VersionPageConsumerProps>> = DetailContext.Consumer;

View File

@@ -0,0 +1,3 @@
export { DetailContextProps, VersionPageConsumerProps } from './types';
export { DetailContext, DetailContextConsumer, DetailContextProvider } from './context';
export { default } from './Version';

View File

@@ -0,0 +1,28 @@
import { PackageMetaInterface } from '../../../types/packageMeta';
export interface DetailContextProps {
packageMeta: PackageMetaInterface;
readMe: string;
packageName: string;
enableLoading: () => void;
}
export interface VersionPageConsumerProps {
packageMeta: PackageMetaInterface;
readMe: string;
packageName: string;
// FIXME: looking for the appropiated type here
enableLoading: any;
}
export interface PropsInterface {
match: boolean;
}
export interface StateInterface {
readMe: string;
packageName: string;
packageMeta?: PackageMetaInterface;
isLoading: boolean;
notFound: boolean;
}

View File

@@ -1,160 +0,0 @@
import React, { Component, ReactElement, Consumer, Provider } from 'react';
import Grid from '@material-ui/core/Grid';
import Loading from '../../components/Loading/Loading';
import DetailContainer from '../../components/DetailContainer/DetailContainer';
import DetailSidebar from '../../components/DetailSidebar/DetailSidebar';
import { callDetailPage } from '../../utils/calls';
import { getRouterPackageName } from '../../utils/package';
import NotFound from '../../components/NotFound';
import { PackageMetaInterface } from '../../../types/packageMeta';
export interface DetailContextProps {
packageMeta: PackageMetaInterface;
readMe: string;
packageName: string;
enableLoading: () => void;
}
export const DetailContext = React.createContext<Partial<DetailContextProps>>({});
export interface VersionPageConsumerProps {
packageMeta: PackageMetaInterface;
readMe: string;
packageName: string;
enableLoading: () => void;
}
export const DetailContextProvider: Provider<Partial<VersionPageConsumerProps>> = DetailContext.Provider;
export const DetailContextConsumer: Consumer<Partial<VersionPageConsumerProps>> = DetailContext.Consumer;
interface PropsInterface {
match: boolean;
}
interface StateInterface {
readMe: string;
packageName: string;
packageMeta: PackageMetaInterface | null;
isLoading: boolean;
notFound: boolean;
}
class VersionPage extends Component<PropsInterface, Partial<StateInterface>> {
constructor(props) {
super(props);
this.state = {
readMe: '',
packageName: getRouterPackageName(props.match),
packageMeta: null,
isLoading: true,
notFound: false,
};
}
public static getDerivedStateFromProps(nextProps, prevState): { packageName?: string; isLoading: boolean; notFound?: boolean } | null {
const { match } = nextProps;
const packageName = getRouterPackageName(match);
if (packageName !== prevState.packageName) {
try {
return {
packageName,
isLoading: false,
};
} catch (err) {
return {
notFound: true,
isLoading: false,
};
}
} else {
return null;
}
}
public async componentDidMount(): Promise<void> {
await this.loadPackageInfo();
}
/* eslint no-unused-vars: 0 */
public async componentDidUpdate(nextProps, prevState: StateInterface): Promise<void> {
const { packageName } = this.state;
if (packageName !== prevState.packageName) {
const { readMe, packageMeta } = (await callDetailPage(packageName)) as Partial<StateInterface>;
this.setState({
readMe,
packageMeta,
packageName,
notFound: false,
isLoading: false,
});
}
}
public render(): ReactElement<HTMLElement> {
const { isLoading, packageMeta, readMe, packageName } = this.state;
if (isLoading) {
return <Loading />;
} else if (!packageMeta) {
return <NotFound />;
} else {
return (
<DetailContextProvider value={{ packageMeta, readMe, packageName, enableLoading: this.enableLoading }}>
<Grid className={'container content'} container={true} spacing={0}>
<Grid item={true} xs={8}>
{this.renderDetail()}
</Grid>
<Grid item={true} xs={4}>
{this.renderSidebar()}
</Grid>
</Grid>
</DetailContextProvider>
);
}
}
public async loadPackageInfo(): Promise<void> {
const { packageName } = this.state;
// FIXME: use utility
document.title = `Verdaccio - ${packageName}`;
this.setState({
readMe: '',
});
try {
const { readMe, packageMeta } = (await callDetailPage(packageName)) as Partial<StateInterface>;
this.setState({
readMe,
packageMeta,
packageName,
notFound: false,
isLoading: false,
});
} catch (err) {
this.setState({
notFound: true,
packageName,
isLoading: false,
});
}
}
public enableLoading = () => {
this.setState({
isLoading: true,
});
};
public renderDetail(): ReactElement<HTMLElement> {
return <DetailContainer />;
}
public renderSidebar(): ReactElement<HTMLElement> {
return <DetailSidebar />;
}
}
export default VersionPage;

View File

@@ -1 +0,0 @@
export { default, DetailContextProps } from './Version';