1
0
mirror of https://github.com/verdaccio/verdaccio.git synced 2024-11-08 23:25:51 +01:00

refactor: rematch as web state storage for UI (#2447)

* trying rematch

refactor: rematch for store packages

migrate login to rematch

Update packages/plugins/ui-theme/src/store/store.ts

Co-authored-by: Sergio Moreno <sergiomorenoalbert@gmail.com>

hide temporary

fix test for login

migrate package download resource

fix tests

* add missing fixture

* migrate detail page support

* fix tests

* migrate search

* migrate search

* clean up tests

* remove tags

* fix lint

* add changeset

* fix: search model typings

* add type

* types

* apply suggestions

Co-authored-by: Sergio Moreno <22656541+semoal@users.noreply.github.com>
This commit is contained in:
Juan Picado 2021-09-25 17:35:03 +02:00 committed by GitHub
parent 9dbf73e955
commit 5fed1955a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 1135 additions and 945 deletions

@ -0,0 +1,5 @@
---
'@verdaccio/ui-theme': minor
---
feat: integrate rematch for ui state storage

2
.gitignore vendored

@ -48,5 +48,5 @@ hyper-results*.json
api-results*.json
#docs
api/
./api
packages/core/core/docs

@ -85,7 +85,6 @@
"jest-environment-jsdom": "27.1.0",
"jest-environment-jsdom-global": "3.0.0",
"jest-environment-node": "27.1.0",
"jest-fetch-mock": "3.0.3",
"jest-junit": "12.2.0",
"kleur": "3.0.3",
"lint-staged": "11.1.2",
@ -123,7 +122,7 @@
"benchmark:submit": "pnpm ts-node ./scripts/submit-metrics.ts",
"start:watch": "concurrently --kill-others \"pnpm _build:watch\" \"pnpm _start:server\" \"pnpm _debug:reload\"",
"_build:watch": "pnpm run --parallel watch --filter ./packages",
"_start:server": "node packages/verdaccio/debug/bootstrap.js --listen 8000",
"_start:server": "node --inspect packages/verdaccio/debug/bootstrap.js --listen 8000",
"_start:web": "pnpm start --filter ...@verdaccio/ui-theme",
"_debug:reload": "nodemon -d 3 packages/verdaccio/debug/bootstrap.js",
"start:ts": "ts-node packages/verdaccio/src/start.ts -- --listen 8000",
@ -138,7 +137,7 @@
"ts:ref": "update-ts-references --discardComments",
"website": "pnpm build --filter ...@verdaccio/website",
"crowdin:upload": "crowdin upload sources --auto-update --config ./crowdin.yaml",
"crowdin:download": "crowdin download --config ./crowdin.yaml",
"crowdin:download": "crowdin download --verbose --config ./crowdin.yaml",
"crowdin:sync": "pnpm crowdin:upload && pnpm crowdin:download --verbose",
"postinstall": "husky install"
},

@ -0,0 +1,42 @@
[
{
"name": "test",
"version": "1.0.22",
"description": "test",
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "http",
"url": "git+https://github.com/test/test.git"
},
"keywords": [],
"author": {
"name": "",
"email": "",
"url": "",
"avatar": "data:image/svg+xml;utf8,%3Csvg%20height%3D%22100%22%20viewBox%3D%22-27%2024%20100%20100%22%20width%3D%22100%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%3Cdefs%3E%3Ccircle%20cx%3D%2223%22%20cy%3D%2274%22%20id%3D%22a%22%20r%3D%2250%22%2F%3E%3C%2Fdefs%3E%3Cuse%20fill%3D%22%23F5EEE5%22%20overflow%3D%22visible%22%20xlink%3Ahref%3D%22%23a%22%2F%3E%3CclipPath%20id%3D%22b%22%3E%3Cuse%20overflow%3D%22visible%22%20xlink%3Ahref%3D%22%23a%22%2F%3E%3C%2FclipPath%3E%3Cg%20clip-path%3D%22url(%23b)%22%3E%3Cdefs%3E%3Cpath%20d%3D%22M36%2095.9c0%204%204.7%205.2%207.1%205.8%207.6%202%2022.8%205.9%2022.8%205.9%203.2%201.1%205.7%203.5%207.1%206.6v9.8H-27v-9.8c1.3-3.1%203.9-5.5%207.1-6.6%200%200%2015.2-3.9%2022.8-5.9%202.4-.6%207.1-1.8%207.1-5.8V85h26v10.9z%22%20id%3D%22c%22%2F%3E%3C%2Fdefs%3E%3Cuse%20fill%3D%22%23E6C19C%22%20overflow%3D%22visible%22%20xlink%3Ahref%3D%22%23c%22%2F%3E%3CclipPath%20id%3D%22d%22%3E%3Cuse%20overflow%3D%22visible%22%20xlink%3Ahref%3D%22%23c%22%2F%3E%3C%2FclipPath%3E%3Cpath%20clip-path%3D%22url(%23d)%22%20d%3D%22M23.2%2035h.2c3.3%200%208.2.2%2011.4%202%203.3%201.9%207.3%205.6%208.5%2012.1%202.4%2013.7-2.1%2035.4-6.3%2042.4-4%206.7-9.8%209.2-13.5%209.4H23h-.1c-3.7-.2-9.5-2.7-13.5-9.4-4.2-7-8.7-28.7-6.3-42.4%201.2-6.5%205.2-10.2%208.5-12.1%203.2-1.8%208.1-2%2011.4-2h.2z%22%20fill%3D%22%23D4B08C%22%2F%3E%3C%2Fg%3E%3Cpath%20d%3D%22M22.6%2040c19.1%200%2020.7%2013.8%2020.8%2015.1%201.1%2011.9-3%2028.1-6.8%2033.7-4%205.9-9.8%208.1-13.5%208.3h-.5c-3.8-.3-9.6-2.5-13.6-8.4-3.8-5.6-7.9-21.8-6.8-33.8C2.3%2053.7%203.5%2040%2022.6%2040z%22%20fill%3D%22%23F2CEA5%22%2F%3E%3C%2Fsvg%3E"
},
"license": "ISC",
"dependencies": {
"lodash": "^4.17.21"
},
"readmeFilename": "README.md",
"bugs": {
"url": "https://github.com/test/test/issues"
},
"homepage": "https://github.com/test/test#readme",
"_id": "test@1.0.22",
"_nodeVersion": "14.17.4",
"_npmVersion": "7.20.5",
"dist": {
"integrity": "sha512-2IDD0lLzGUL7YJ+17Oh9VtbOwdKLqBLS+ZFATDXi5R22TL2hZ9LBFE10bzsDovNc4xtgwZAk1/K+5LHTye4ztg==",
"shasum": "c9152f57636bce762ccb5a83113c42a5831579bc",
"tarball": "http://localhost:4873/test/-/test-1.0.22.tgz"
},
"contributors": [],
"time": "2021-08-14T20:15:19.336Z",
"users": {}
}
]

@ -10,9 +10,9 @@ module.exports = Object.assign({}, config, {
'^.+\\.(js|ts|tsx)$': 'babel-jest',
},
moduleFileExtensions: ['js', 'ts', 'tsx'],
testURL: 'http://localhost',
testURL: 'http://localhost:9000/',
rootDir: '..',
setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'],
setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect', '<rootDir>/jest/setup-env.ts'],
setupFiles: ['<rootDir>/jest/setup.ts'],
transformIgnorePatterns: ['<rootDir>/node_modules/(?!react-syntax-highlighter)'],
modulePathIgnorePatterns: [

@ -0,0 +1,9 @@
import { rest } from 'msw';
const packagesPayload = require('./api/packages.json');
export const handlers = [
rest.get('http://localhost:9000/-/verdaccio/packages', (req, res, ctx) => {
return res(ctx.json(packagesPayload));
}),
];

@ -0,0 +1,6 @@
import { setupServer } from 'msw/node';
import { handlers } from './server-handlers';
const server = setupServer(...handlers);
export { server };

@ -0,0 +1,15 @@
import '@testing-library/jest-dom/extend-expect';
import 'whatwg-fetch';
import '@testing-library/jest-dom';
import { server } from './server';
beforeAll(() => {
server.listen({
onUnhandledRequest: 'warn',
});
});
afterEach(() => server.resetHandlers());
afterAll(() => {
server.close();
});

@ -2,29 +2,24 @@
* Setup configuration for Jest
* This file includes global settings for the JEST environment.
*/
import { GlobalWithFetchMock } from 'jest-fetch-mock';
import 'mutationobserver-shim';
// @ts-ignore : Property '__VERDACCIO_BASENAME_UI_OPTIONS' does not exist on type 'Global'.
global.__VERDACCIO_BASENAME_UI_OPTIONS = {
base: 'http://localhost',
base: 'http://localhost:9000/',
protocol: 'http',
host: 'localhost',
primaryColor: '#4b5e40',
url_prefix: '',
darkMode: false,
language: 'en-US',
uri: 'http://localhost:4873',
uri: 'http://localhost:9000/',
pkgManagers: ['pnpm', 'yarn', 'npm'],
title: 'Verdaccio Dev UI',
scope: '',
version: 'v1.0.0',
};
const customGlobal: GlobalWithFetchMock = global as GlobalWithFetchMock;
customGlobal.fetch = require('jest-fetch-mock');
customGlobal.fetchMock = customGlobal.fetch;
// mocking few DOM methods
// @ts-ignore : Property 'document' does not exist on type 'Global'.
if (global.document) {

@ -21,6 +21,7 @@
"@types/react-autosuggest": "10.1.5",
"@types/react-dom": "17.0.9",
"@types/react-helmet": "6.1.2",
"@types/redux": "3.6.0",
"@types/react-router-dom": "5.1.8",
"@types/react-virtualized": "9.21.13",
"@emotion/core": "10.1.1",
@ -30,9 +31,11 @@
"@material-ui/core": "4.11.4",
"@material-ui/icons": "4.11.2",
"@material-ui/styles": "4.11.4",
"@testing-library/dom": "8.2.0",
"@rematch/core": "2.1.0",
"@rematch/loading": "2.1.0",
"@testing-library/dom": "8.5.0",
"@testing-library/jest-dom": "5.14.1",
"@testing-library/react": "12.0.0",
"@testing-library/react": "12.1.0",
"@verdaccio/node-api": "workspace:6.0.0-6-next.20",
"autosuggest-highlight": "3.1.1",
"babel-loader": "8.2.2",
@ -65,13 +68,16 @@
"react": "17.0.2",
"react-autosuggest": "10.1.0",
"react-dom": "17.0.2",
"react-hook-form": "7.14.2",
"react-hook-form": "7.15.3",
"react-hot-loader": "4.13.0",
"react-i18next": "11.12.0",
"react-router": "5.2.1",
"react-router-dom": "5.3.0",
"react-virtualized": "9.22.3",
"react-redux": "7.2.1",
"redux": "4.1.1",
"rimraf": "3.0.2",
"msw": "0.35.0",
"standard-version": "9.3.1",
"style-loader": "3.2.1",
"stylelint": "13.13.1",

@ -1,10 +1,16 @@
import React from 'react';
import storage from 'verdaccio-ui/utils/storage';
import { render, waitFor, fireEvent } from 'verdaccio-ui/utils/test-react-testing-library';
import {
renderWithStore,
act,
waitFor,
fireEvent,
screen,
} from 'verdaccio-ui/utils/test-react-testing-library';
// eslint-disable-next-line jest/no-mocks-import
import { generateTokenWithTimeRange } from '../../jest/unit/components/__mocks__/token';
import { store } from '../store';
import App from './App';
@ -30,68 +36,88 @@ jest.mock('verdaccio-ui/utils/storage', () => {
return new LocalStorageMock();
});
jest.mock('verdaccio-ui/providers/API/api', () => ({
// eslint-disable-next-line jest/no-mocks-import
request: require('../../jest/unit/components/__mocks__/api').default.request,
}));
// force the windows to expand to display items
// https://github.com/bvaughn/react-virtualized/issues/493#issuecomment-640084107
jest.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(600);
jest.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockReturnValue(600);
/* eslint-disable react/jsx-no-bind*/
describe('<App />', () => {
// test('should display the Header component ', async () => {
// const { queryByTestId } = render(<App />);
//
// expect(queryByTestId('loading')).toBeTruthy();
//
// // wait for the Header component appearance and return the element
// const headerElement = await waitFor(() => queryByTestId('header'));
// expect(headerElement).toBeTruthy();
// });
describe('login - log out', () => {
test('handleLogout - logouts the user and clear localstorage', async () => {
const { queryByTestId } = renderWithStore(<App />, store);
store.dispatch.login.logInUser({
username: 'verdaccio',
token: generateTokenWithTimeRange(24),
});
test('handleLogout - logouts the user and clear localstorage', async () => {
storage.setItem('username', 'verdaccio');
storage.setItem('token', generateTokenWithTimeRange(24));
// wait for the Account's circle element component appearance and return the element
const accountCircleElement = await waitFor(() => queryByTestId('logInDialogIcon'));
expect(accountCircleElement).toBeTruthy();
const { queryByTestId } = render(<App />);
if (accountCircleElement) {
fireEvent.click(accountCircleElement);
// wait for the Account's circle element component appearance and return the element
const accountCircleElement = await waitFor(() => queryByTestId('header--menu-accountcircle'));
expect(accountCircleElement).toBeTruthy();
// wait for the Button's logout element component appearance and return the element
const buttonLogoutElement = await waitFor(() => queryByTestId('logOutDialogIcon'));
expect(buttonLogoutElement).toBeTruthy();
if (accountCircleElement) {
fireEvent.click(accountCircleElement);
if (buttonLogoutElement) {
fireEvent.click(buttonLogoutElement);
// wait for the Button's logout element component appearance and return the element
const buttonLogoutElement = await waitFor(() => queryByTestId('header--button-logout'));
expect(buttonLogoutElement).toBeTruthy();
if (buttonLogoutElement) {
fireEvent.click(buttonLogoutElement);
expect(queryByTestId('greetings-label')).toBeFalsy();
expect(queryByTestId('greetings-label')).toBeFalsy();
}
}
}
});
test('isUserAlreadyLoggedIn: token already available in storage', async () => {
const { queryByTestId, queryAllByText } = renderWithStore(<App />, store);
store.dispatch.login.logInUser({
username: 'verdaccio',
token: generateTokenWithTimeRange(24),
});
// wait for the Account's circle element component appearance and return the element
const accountCircleElement = await waitFor(() => queryByTestId('logInDialogIcon'));
expect(accountCircleElement).toBeTruthy();
if (accountCircleElement) {
fireEvent.click(accountCircleElement);
// wait for the Greeting's label element component appearance and return the element
const greetingsLabelElement = await waitFor(() => queryByTestId('greetings-label'));
expect(greetingsLabelElement).toBeTruthy();
if (greetingsLabelElement) {
expect(queryAllByText('verdaccio')).toBeTruthy();
}
}
});
});
test('isUserAlreadyLoggedIn: token already available in storage', async () => {
storage.setItem('username', 'verdaccio');
storage.setItem('token', generateTokenWithTimeRange(24));
describe('list packages', () => {
test('should display the Header component', async () => {
renderWithStore(<App />, store);
const { queryByTestId, queryAllByText } = render(<App />);
await waitFor(() => {
expect(screen.queryByTestId('loading')).toBeTruthy();
});
// wait for the Account's circle element component appearance and return the element
const accountCircleElement = await waitFor(() => queryByTestId('header--menu-accountcircle'));
expect(accountCircleElement).toBeTruthy();
// wait for the Header component appearance and return the element
const headerElement = await waitFor(() => screen.queryByTestId('header'));
expect(headerElement).toBeTruthy();
});
if (accountCircleElement) {
fireEvent.click(accountCircleElement);
test('should display package lists', async () => {
act(() => {
renderWithStore(<App />, store);
});
// wait for the Greeting's label element component appearance and return the element
const greetingsLabelElement = await waitFor(() => queryByTestId('greetings-label'));
expect(greetingsLabelElement).toBeTruthy();
await waitFor(() => {
expect(screen.getByTestId('package-item-list')).toBeInTheDocument();
});
if (greetingsLabelElement) {
expect(queryAllByText('verdaccio')).toBeTruthy();
}
}
expect(store.getState().packages.response).toHaveLength(1);
}, 10000);
});
});

@ -1,7 +1,6 @@
/* eslint-disable react/jsx-max-depth */
import styled from '@emotion/styled';
import isNil from 'lodash/isNil';
import React, { useState, useEffect, Suspense } from 'react';
import React, { useEffect, Suspense } from 'react';
import { Router } from 'react-router-dom';
import Box from 'verdaccio-ui/components/Box';
@ -9,10 +8,7 @@ import Loading from 'verdaccio-ui/components/Loading';
import loadDayJSLocale from 'verdaccio-ui/design-tokens/load-dayjs-locale';
import StyleBaseline from 'verdaccio-ui/design-tokens/StyleBaseline';
import { Theme } from 'verdaccio-ui/design-tokens/theme';
import { isTokenExpire } from 'verdaccio-ui/utils/login';
import storage from 'verdaccio-ui/utils/storage';
import AppContextProvider from './AppContextProvider';
import AppRoute, { history } from './AppRoute';
import Footer from './Footer';
import Header from './Header';
@ -32,35 +28,8 @@ const StyledBoxContent = styled(Box)<{ theme?: Theme }>(({ theme }) => ({
},
}));
/* eslint-disable react/jsx-no-bind */
/* eslint-disable react-hooks/exhaustive-deps */
const App: React.FC = () => {
const [user, setUser] = useState<undefined | { username: string | null }>();
/**
* Logout user
* Required by: <Header />
*/
const logout = () => {
storage.removeItem('username');
storage.removeItem('token');
setUser(undefined);
};
const checkUserAlreadyLoggedIn = () => {
// checks for token validity
const token = storage.getItem('token');
const username = storage.getItem('username');
if (isTokenExpire(token) || isNil(username)) {
logout();
return;
}
setUser({ username });
};
useEffect(() => {
checkUserAlreadyLoggedIn();
loadDayJSLocale();
}, []);
@ -70,12 +39,10 @@ const App: React.FC = () => {
<StyledBox display="flex" flexDirection="column" height="100%">
<>
<Router history={history}>
<AppContextProvider user={user}>
<Header />
<StyledBoxContent flexGrow={1}>
<AppRoute />
</StyledBoxContent>
</AppContextProvider>
<Header />
<StyledBoxContent flexGrow={1}>
<AppRoute />
</StyledBoxContent>
</Router>
<Footer />
</>

@ -1,18 +0,0 @@
import { createContext } from 'react';
export interface AppProps {
user?: User;
scope: string;
}
export interface User {
username: string | null;
}
export interface AppContextProps extends AppProps {
setUser: (user?: User) => void;
}
const AppContext = createContext<undefined | AppContextProps>(undefined);
export default AppContext;

@ -1,44 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useConfig } from 'verdaccio-ui/providers/config';
import AppContext, { AppProps, User } from './AppContext';
interface Props {
user?: User;
}
/* eslint-disable react-hooks/exhaustive-deps */
const AppContextProvider: React.FC<Props> = ({ children, user }) => {
const { configOptions } = useConfig();
const [state, setState] = useState<AppProps>({
scope: configOptions.scope ?? '',
user,
});
useEffect(() => {
setState({
...state,
user,
});
}, [user]);
const setUser = (user?: User) => {
setState({
...state,
user,
});
};
return (
<AppContext.Provider
value={{
...state,
setUser,
}}>
{children}
</AppContext.Provider>
);
};
export default AppContextProvider;

@ -1,9 +1,7 @@
import { createBrowserHistory } from 'history';
import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import React from 'react';
import { Route as ReactRouterDomRoute, Switch, Router } from 'react-router-dom';
import AppContext from './AppContext';
import loadable from './utils/loadable';
const NotFound = loadable(
@ -28,22 +26,11 @@ export const history = createBrowserHistory({
});
const AppRoute: React.FC = () => {
const appContext = useContext(AppContext);
const { t } = useTranslation();
if (!appContext) {
throw Error(t('app-context-not-correct-used'));
}
const { user } = appContext;
const isUserLoggedIn = user?.username;
return (
<Router history={history}>
<Switch>
<ReactRouterDomRoute exact={true} path={Route.ROOT}>
<HomePage isUserLoggedIn={!!isUserLoggedIn} />
<HomePage />
</ReactRouterDomRoute>
<ReactRouterDomRoute exact={true} path={Route.PACKAGE}>
<VersionContextProvider>

@ -2,79 +2,78 @@ import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import {
render,
renderWithStore,
fireEvent,
waitFor,
cleanup,
screen,
waitForElementToBeRemoved,
} from 'verdaccio-ui/utils/test-react-testing-library';
import { AppContextProvider } from '../../App';
import translationEN from '../../i18n/crowdin/ui.json';
import { store } from '../../store';
import Header from './Header';
const props = {
user: {
username: 'verddacio-user',
},
packages: [],
};
/* eslint-disable react/jsx-no-bind*/
describe('<Header /> component with logged in state', () => {
afterEach(cleanup);
test('should load the component in logged out state', () => {
render(
renderWithStore(
<Router>
<AppContextProvider>
<Header />
</AppContextProvider>
</Router>
<Header />
</Router>,
store
);
expect(screen.queryByTestId('header--menu-accountcircle')).toBeNull();
expect(screen.queryByTestId('logInDialogIcon')).toBeNull();
expect(screen.getByText('Login')).toBeTruthy();
expect(screen.queryByTestId('header--button-login')).toBeInTheDocument();
});
test('should load the component in logged in state', () => {
const { getByTestId, queryByText } = render(
test('should load the component in logged in state', async () => {
renderWithStore(
<Router>
<AppContextProvider user={props.user}>
<Header />
</AppContextProvider>
</Router>
<Header />
</Router>,
store
);
store.dispatch.login.logInUser({ username: 'store', token: '12345' });
expect(getByTestId('header--menu-accountcircle')).toBeTruthy();
expect(queryByText('Login')).toBeNull();
await waitFor(() => {
expect(screen.getByTestId('logInDialogIcon')).toBeTruthy();
expect(screen.queryByText('Login')).toBeNull();
});
});
test('should open login dialog', async () => {
const { getByTestId } = render(
renderWithStore(
<Router>
<AppContextProvider>
<Header />
</AppContextProvider>
</Router>
<Header />
</Router>,
store
);
const loginBtn = getByTestId('header--button-login');
store.dispatch.login.logOutUser();
const loginBtn = screen.getByTestId('header--button-login');
fireEvent.click(loginBtn);
const loginDialog = await waitFor(() => getByTestId('login--dialog'));
const loginDialog = await waitFor(() => screen.getByTestId('login--dialog'));
expect(loginDialog).toBeTruthy();
});
test('should logout the user', async () => {
const { getByText, getByTestId } = render(
const { getByText, getByTestId } = renderWithStore(
<Router>
<AppContextProvider user={props.user}>
<Header />
</AppContextProvider>
</Router>
<Header />
</Router>,
store
);
const headerMenuAccountCircle = getByTestId('header--menu-accountcircle');
store.dispatch.login.logInUser({ username: 'store', token: '12345' });
const headerMenuAccountCircle = getByTestId('logInDialogIcon');
fireEvent.click(headerMenuAccountCircle);
// wait for button Logout's appearance and return the element
@ -84,12 +83,11 @@ describe('<Header /> component with logged in state', () => {
});
test("The question icon should open a new tab of verdaccio's website - installation doc", () => {
const { getByTestId } = render(
const { getByTestId } = renderWithStore(
<Router>
<AppContextProvider user={props.user}>
<Header />
</AppContextProvider>
</Router>
<Header />
</Router>,
store
);
const documentationBtn = getByTestId('header--tooltip-documentation');
@ -99,12 +97,11 @@ describe('<Header /> component with logged in state', () => {
});
test('should open the registrationInfo modal when clicking on the info icon', async () => {
const { getByTestId } = render(
const { getByTestId } = renderWithStore(
<Router>
<AppContextProvider user={props.user}>
<Header />
</AppContextProvider>
</Router>
<Header />
</Router>,
store
);
const infoBtn = getByTestId('header--tooltip-info');
@ -116,12 +113,11 @@ describe('<Header /> component with logged in state', () => {
});
test('should close the registrationInfo modal when clicking on the button close', async () => {
const { getByTestId, getByText, queryByTestId } = render(
const { getByTestId, getByText, queryByTestId } = renderWithStore(
<Router>
<AppContextProvider user={props.user}>
<Header />
</AppContextProvider>
</Router>
<Header />
</Router>,
store
);
const infoBtn = getByTestId('header--tooltip-info');
@ -138,17 +134,15 @@ describe('<Header /> component with logged in state', () => {
});
test('should hide login if is disabled', () => {
// @ts-expect-error
window.__VERDACCIO_BASENAME_UI_OPTIONS = {
base: 'foo',
login: false,
};
render(
renderWithStore(
<Router>
<AppContextProvider user={props.user}>
<Header />
</AppContextProvider>
</Router>
<Header />
</Router>,
store
);
expect(screen.queryByTestId('header--button-login')).not.toBeInTheDocument();

@ -1,11 +1,11 @@
import React, { useState, useContext } from 'react';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import Button from 'verdaccio-ui/components/Button';
import { useConfig } from 'verdaccio-ui/providers/config';
import storage from 'verdaccio-ui/utils/storage';
import AppContext from '../../App/AppContext';
import { Dispatch, RootState } from '../../store/store';
import HeaderInfoDialog from './HeaderInfoDialog';
import HeaderLeft from './HeaderLeft';
@ -21,26 +21,15 @@ interface Props {
/* eslint-disable react/jsx-no-bind*/
const Header: React.FC<Props> = ({ withoutSearch }) => {
const { t } = useTranslation();
const appContext = useContext(AppContext);
const [isInfoDialogOpen, setOpenInfoDialog] = useState<boolean>(false);
const [showMobileNavBar, setShowMobileNavBar] = useState<boolean>(false);
const [showLoginModal, setShowLoginModal] = useState<boolean>(false);
if (!appContext) {
throw Error(t('app-context-not-correct-used'));
}
const { user, scope, setUser } = appContext;
const loginStore = useSelector((state: RootState) => state.login);
const configStore = useSelector((state: RootState) => state.configuration);
const { configOptions } = useConfig();
/**
* Logouts user
* Required by: <Header />
*/
const dispatch = useDispatch<Dispatch>();
const handleLogout = () => {
storage.removeItem('username');
storage.removeItem('token');
setUser(undefined);
dispatch.login.logOutUser();
};
return (
@ -54,7 +43,7 @@ const Header: React.FC<Props> = ({ withoutSearch }) => {
onOpenRegistryInfoDialog={() => setOpenInfoDialog(true)}
onToggleLogin={() => setShowLoginModal(!showLoginModal)}
onToggleMobileNav={() => setShowMobileNavBar(!showMobileNavBar)}
username={user?.username}
username={loginStore?.username}
withoutSearch={withoutSearch}
/>
</InnerNavBar>
@ -62,7 +51,7 @@ const Header: React.FC<Props> = ({ withoutSearch }) => {
isOpen={isInfoDialogOpen}
onCloseDialog={() => setOpenInfoDialog(false)}
registryUrl={configOptions.base}
scope={scope}
scope={configStore.scope}
/>
</NavBar>
{showMobileNavBar && !withoutSearch && (
@ -75,7 +64,9 @@ const Header: React.FC<Props> = ({ withoutSearch }) => {
</Button>
</MobileNavBar>
)}
{!user && <LoginDialog onClose={() => setShowLoginModal(false)} open={showLoginModal} />}
{!loginStore.user && (
<LoginDialog onClose={() => setShowLoginModal(false)} open={showLoginModal} />
)}
</>
);
};

@ -30,7 +30,7 @@ const HeaderMenu: React.FC<Props> = ({
<>
<IconButton
color="inherit"
data-testid="header--menu-accountcircle"
data-testid="logInDialogIcon"
id="header--button-account"
onClick={onLoggedInMenu}>
<AccountCircle />
@ -52,8 +52,8 @@ const HeaderMenu: React.FC<Props> = ({
</MenuItem>
<MenuItem
button={true}
data-testid="header--button-logout"
id="header--button-logout"
data-testid="logOutDialogIcon"
id="logOutDialogIcon"
onClick={onLogout}>
{t('button.logout')}
</MenuItem>

@ -2,7 +2,7 @@ import React from 'react';
import api from 'verdaccio-ui/providers/API/api';
import {
render,
renderWithStore,
waitFor,
fireEvent,
cleanup,
@ -10,15 +10,10 @@ import {
act,
} from 'verdaccio-ui/utils/test-react-testing-library';
import AppContext, { AppContextProps } from '../../AppContext';
import { store } from '../../../store';
import LoginDialog from './LoginDialog';
const appContextValue: AppContextProps = {
scope: '',
setUser: jest.fn(),
};
describe('<LoginDialog /> component', () => {
beforeEach(() => {
jest.resetModules();
@ -30,11 +25,7 @@ describe('<LoginDialog /> component', () => {
const props = {
onClose: jest.fn(),
};
const { container } = render(
<AppContext.Provider value={appContextValue}>
<LoginDialog onClose={props.onClose} />
</AppContext.Provider>
);
const { container } = renderWithStore(<LoginDialog onClose={props.onClose} />, store);
expect(container.firstChild).toMatchSnapshot();
});
@ -44,10 +35,9 @@ describe('<LoginDialog /> component', () => {
onClose: jest.fn(),
};
const { getByTestId } = render(
<AppContext.Provider value={appContextValue}>
<LoginDialog onClose={props.onClose} open={props.open} />
</AppContext.Provider>
const { getByTestId } = renderWithStore(
<LoginDialog onClose={props.onClose} open={props.open} />,
store
);
const loginDialogHeading = await waitFor(() => getByTestId('login-dialog-form-login-button'));
@ -60,18 +50,18 @@ describe('<LoginDialog /> component', () => {
onClose: jest.fn(),
};
const { getByTestId } = render(
<AppContext.Provider value={appContextValue}>
<LoginDialog onClose={props.onClose} open={props.open} />
</AppContext.Provider>
const { getByTestId } = renderWithStore(
<LoginDialog onClose={props.onClose} open={props.open} />,
store
);
const loginDialogButton = await waitFor(() => getByTestId('close-login-dialog-button'));
expect(loginDialogButton).toBeTruthy();
act(() => {
await act(() => {
fireEvent.click(loginDialogButton, { open: false });
});
expect(props.onClose).toHaveBeenCalled();
});
@ -88,11 +78,9 @@ describe('<LoginDialog /> component', () => {
})
);
render(
<AppContext.Provider value={appContextValue}>
<LoginDialog onClose={props.onClose} open={props.open} />
</AppContext.Provider>
);
await act(async () => {
renderWithStore(<LoginDialog onClose={props.onClose} open={props.open} />, store);
});
const userNameInput = screen.getByPlaceholderText('Your username');
expect(userNameInput).toBeInTheDocument();
@ -104,13 +92,18 @@ describe('<LoginDialog /> component', () => {
const passwordInput = screen.getByPlaceholderText('Your strong password');
expect(userNameInput).toBeInTheDocument();
fireEvent.focus(passwordInput);
fireEvent.change(passwordInput, { target: { value: '1234' } });
act(async () => {
const signInButton = await screen.getByTestId('login-dialog-form-login-button');
expect(signInButton).not.toBeDisabled();
await act(async () => {
fireEvent.change(passwordInput, { target: { value: '1234' } });
});
const signInButton = screen.getByTestId('login-dialog-form-login-button');
expect(signInButton).not.toBeDisabled();
await act(async () => {
fireEvent.click(signInButton);
});
expect(props.onClose).toHaveBeenCalledTimes(1);
// screen.debug();
});
test.todo('validateCredentials: should validate credentials');

@ -1,15 +1,13 @@
import i18next from 'i18next';
import isEmpty from 'lodash/isEmpty';
import React, { useState, useContext, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import React, { useEffect, useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Dialog from 'verdaccio-ui/components/Dialog';
import DialogContent from 'verdaccio-ui/components/DialogContent';
import { useAPI, LoginBody } from 'verdaccio-ui/providers/API/APIProvider';
import { LoginError } from 'verdaccio-ui/utils/login';
import storage from 'verdaccio-ui/utils/storage';
import AppContext from '../../../App/AppContext';
import { LoginBody } from '../../../store/models/login';
import { Dispatch, RootState } from '../../../store/store';
import LoginDialogCloseButton from './LoginDialogCloseButton';
import LoginDialogForm, { FormValues } from './LoginDialogForm';
@ -21,62 +19,48 @@ interface Props {
}
const LoginDialog: React.FC<Props> = ({ onClose, open = false }) => {
const { t } = useTranslation();
const appContext = useContext(AppContext);
const { doLogin } = useAPI();
const loginStore = useSelector((state: RootState) => state.login);
const dispatch = useDispatch<Dispatch>();
const makeLogin = useCallback(
async (username?: string, password?: string): Promise<LoginBody> => {
async (username?: string, password?: string): Promise<LoginBody | void> => {
// checks isEmpty
if (isEmpty(username) || isEmpty(password)) {
const error = {
dispatch.login.addError({
type: 'error',
description: i18next.t('form-validation.username-or-password-cant-be-empty'),
};
return { error };
});
return;
}
try {
const response: LoginBody = await doLogin(username as string, password as string);
return response;
dispatch.login.getUser({ username, password });
// const response: LoginBody = await doLogin(username as string, password as string);
dispatch.login.clearError();
} catch (e: any) {
// eslint-disable-next-line no-console
console.error('login error', e.message);
const error = {
dispatch.login.addError({
type: 'error',
description: i18next.t('form-validation.unable-to-sign-in'),
};
return { error };
});
// eslint-disable-next-line no-console
console.error('login error', e.message);
}
},
[doLogin]
[dispatch]
);
if (!appContext) {
throw Error(t('app-context-not-correct-used'));
}
const [error, setError] = useState<LoginError>();
const handleDoLogin = useCallback(
async (data: FormValues) => {
const { username, token, error } = await makeLogin(data.username, data.password);
if (error) {
setError(error);
}
if (username && token) {
storage.setItem('username', username);
storage.setItem('token', token);
appContext.setUser({ username });
onClose();
}
await makeLogin(data.username, data.password);
},
[appContext, onClose, makeLogin]
[makeLogin]
);
useEffect(() => {
if (loginStore.token && typeof loginStore.error === 'undefined') {
onClose();
}
}, [loginStore, onClose]);
return (
<Dialog
data-testid="login--dialog"
@ -88,7 +72,7 @@ const LoginDialog: React.FC<Props> = ({ onClose, open = false }) => {
<LoginDialogCloseButton onClose={onClose} />
<DialogContent>
<LoginDialogHeader />
<LoginDialogForm error={error} onSubmit={handleDoLogin} />
<LoginDialogForm error={loginStore.error} onSubmit={handleDoLogin} />
</DialogContent>
</Dialog>
);

@ -18,7 +18,6 @@ describe('<RegistryInfoContent /> component', () => {
const props = { registryUrl: 'http://localhost:4872', scope: '@' };
render(<RegistryInfoContent registryUrl={props.registryUrl} scope={props.scope} />);
screen.debug();
expect(screen.getByText('pnpm set @:registry http://localhost:4872')).toBeInTheDocument();
expect(screen.getByText('pnpm adduser --registry http://localhost:4872')).toBeInTheDocument();
expect(

@ -2,7 +2,9 @@ import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import api from 'verdaccio-ui/providers/API/api';
import { render, fireEvent, waitFor } from 'verdaccio-ui/utils/test-react-testing-library';
import { renderWithStore, fireEvent, waitFor } from 'verdaccio-ui/utils/test-react-testing-library';
import { store } from '../../../store/store';
jest.mock('lodash/debounce', () =>
jest.fn((fn) => {
@ -41,12 +43,15 @@ describe('<Search /> component', () => {
});
test('should load the component in default state', () => {
const { container } = render(<ComponentToBeRendered />);
const { container } = renderWithStore(<ComponentToBeRendered />, store);
expect(container.firstChild).toMatchSnapshot();
});
test('handleSearch: when user type package name in search component, show suggestions', async () => {
const { getByPlaceholderText, getAllByText } = render(<ComponentToBeRendered />);
const { getByPlaceholderText, getAllByText } = renderWithStore(
<ComponentToBeRendered />,
store
);
const autoCompleteInput = getByPlaceholderText('Search Packages');
@ -62,7 +67,10 @@ describe('<Search /> component', () => {
});
test('onBlur: should cancel all search requests', async () => {
const { getByPlaceholderText, getByRole, getAllByText } = render(<ComponentToBeRendered />);
const { getByPlaceholderText, getByRole, getAllByText } = renderWithStore(
<ComponentToBeRendered />,
store
);
const autoCompleteInput = getByPlaceholderText('Search Packages');
@ -80,7 +88,7 @@ describe('<Search /> component', () => {
});
test('handleSearch: cancel all search requests when there is no value in search component with type method', async () => {
const { getByPlaceholderText, getByRole } = render(<ComponentToBeRendered />);
const { getByPlaceholderText, getByRole } = renderWithStore(<ComponentToBeRendered />, store);
const autoCompleteInput = getByPlaceholderText('Search Packages');
fireEvent.focus(autoCompleteInput);
@ -92,7 +100,7 @@ describe('<Search /> component', () => {
});
test('handleSearch: when method is not type method', async () => {
const { getByPlaceholderText, getByRole } = render(<ComponentToBeRendered />);
const { getByPlaceholderText, getByRole } = renderWithStore(<ComponentToBeRendered />, store);
const autoCompleteInput = getByPlaceholderText('Search Packages');
@ -105,7 +113,7 @@ describe('<Search /> component', () => {
});
test('handleSearch: loading is been displayed', async () => {
const { getByPlaceholderText, getByText } = render(<ComponentToBeRendered />);
const { getByPlaceholderText, getByText } = renderWithStore(<ComponentToBeRendered />, store);
const autoCompleteInput = getByPlaceholderText('Search Packages');
fireEvent.focus(autoCompleteInput);
@ -117,7 +125,10 @@ describe('<Search /> component', () => {
});
test('handlePackagesClearRequested: should clear suggestions', async () => {
const { getByPlaceholderText, getAllByText, getByRole } = render(<ComponentToBeRendered />);
const { getByPlaceholderText, getAllByText, getByRole } = renderWithStore(
<ComponentToBeRendered />,
store
);
const autoCompleteInput = getByPlaceholderText('Search Packages');
fireEvent.focus(autoCompleteInput);
@ -135,7 +146,10 @@ describe('<Search /> component', () => {
});
test('handleClickSearch: should change the window location on click or return key', async () => {
const { getByPlaceholderText, getAllByText, getByRole } = render(<ComponentToBeRendered />);
const { getByPlaceholderText, getAllByText, getByRole } = renderWithStore(
<ComponentToBeRendered />,
store
);
const autoCompleteInput = getByPlaceholderText('Search Packages');
fireEvent.focus(autoCompleteInput);

@ -1,11 +1,13 @@
import debounce from 'lodash/debounce';
import React, { useState, FormEvent, useCallback, useRef, useEffect } from 'react';
import React, { useState, FormEvent, useCallback } from 'react';
import { SuggestionSelectedEventData } from 'react-autosuggest';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router';
import AutoComplete from 'verdaccio-ui/components/AutoComplete';
import { useAPI } from 'verdaccio-ui/providers/API/APIProvider';
import { Dispatch, RootState } from '../../../store/store';
import SearchAdornment from './SearchAdornment';
@ -16,22 +18,18 @@ const CONSTANTS = {
const Search: React.FC<RouteComponentProps> = ({ history }) => {
const { t } = useTranslation();
const [suggestions, setSuggestions] = useState([]);
const [loaded, setLoaded] = useState(false);
const [search, setSearch] = useState('');
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const mountedRef = useRef(true);
const [requestList, setRequestList] = useState<{ abort: () => void }[]>([]);
const { callSearch } = useAPI();
// const mountedRef = useRef(true);
const { isError, suggestions } = useSelector((state: RootState) => state.search);
const isLoading = useSelector((state: RootState) => state?.loading?.models.search);
const dispatch = useDispatch<Dispatch>();
/**
* Cancel all the requests which are in pending state.
*/
const cancelAllSearchRequests = useCallback(() => {
requestList.forEach((request) => request.abort());
setRequestList([]);
}, [requestList, setRequestList]);
dispatch.search.clearRequestQueue();
}, [dispatch]);
/**
* As user focuses out from input, we cancel all the request from requestList
@ -41,12 +39,9 @@ const Search: React.FC<RouteComponentProps> = ({ history }) => {
(event: FormEvent<HTMLInputElement>) => {
// stops event bubbling
event.stopPropagation();
setLoaded(false);
setLoading(false);
setError(false);
cancelAllSearchRequests();
},
[setLoaded, setLoading, cancelAllSearchRequests, setError]
[cancelAllSearchRequests]
);
/**
@ -58,9 +53,6 @@ const Search: React.FC<RouteComponentProps> = ({ history }) => {
event.stopPropagation();
if (method === 'type') {
const value = newValue.trim();
setLoading(true);
setError(false);
setSearch(value);
setLoaded(false);
/**
@ -79,8 +71,8 @@ const Search: React.FC<RouteComponentProps> = ({ history }) => {
* Cancel all the request from list and make request list empty.
*/
const handlePackagesClearRequested = useCallback(() => {
setSuggestions([]);
}, [setSuggestions]);
dispatch.search.saveSearch({ suggestions: [] });
}, [dispatch]);
/**
* When an user select any package by clicking or pressing return key.
@ -108,46 +100,12 @@ const Search: React.FC<RouteComponentProps> = ({ history }) => {
* For AbortController see: https://developer.mozilla.org/en-US/docs/Web/API/AbortController
*/
const handleFetchPackages = useCallback(
async ({ value }: { value: string }) => {
try {
const controller = new window.AbortController();
const signal = controller.signal;
if (!mountedRef.current) {
return null;
}
// Keep track of search requests.
setRequestList([...requestList, controller]);
const suggestions = await callSearch(value, signal);
// FIXME: Argument of type 'unknown' is not assignable to parameter of type 'SetStateAction<never[]>'
setSuggestions(suggestions as any);
setLoaded(true);
} catch (error: any) {
/**
* AbortError is not the API error.
* It means browser has cancelled the API request.
*/
if (error.name === CONSTANTS.ABORT_ERROR) {
setError(false);
setLoaded(false);
} else {
setError(true);
setLoaded(false);
}
} finally {
if (mountedRef.current) {
setLoading(false);
}
}
({ value }: { value: string }) => {
dispatch.search.getSuggestions({ value });
},
[requestList, setRequestList, setSuggestions, setLoaded, setError, setLoading, callSearch]
[dispatch]
);
useEffect(() => {
return () => {
mountedRef.current = false;
};
}, []);
return (
<AutoComplete
onBlur={handleOnBlur}
@ -158,9 +116,9 @@ const Search: React.FC<RouteComponentProps> = ({ history }) => {
placeholder={t('search.packages')}
startAdornment={<SearchAdornment />}
suggestions={suggestions}
suggestionsError={error}
suggestionsError={isError}
suggestionsLoaded={loaded}
suggestionsLoading={loading}
suggestionsLoading={isLoading}
value={search}
/>
);

@ -1,2 +1 @@
export { default } from './App';
export { default as AppContextProvider } from './AppContextProvider';

@ -1,8 +1,9 @@
import React from 'react';
import { render, cleanup } from 'verdaccio-ui/utils/test-react-testing-library';
import { renderWithStore, cleanup } from 'verdaccio-ui/utils/test-react-testing-library';
import { DetailContext, DetailContextProps } from '../../pages/Version';
import { store } from '../../store/store';
import ActionBar from './ActionBar';
@ -44,7 +45,10 @@ describe('<ActionBar /> component', () => {
});
test('should render the component in default state', () => {
const { container } = render(<ComponentToBeRendered contextValue={detailContextValue} />);
const { container } = renderWithStore(
<ComponentToBeRendered contextValue={detailContextValue} />,
store
);
expect(container.firstChild).toMatchSnapshot();
});
@ -62,22 +66,25 @@ describe('<ActionBar /> component', () => {
},
};
const { container } = render(
<ComponentToBeRendered contextValue={{ ...detailContextValue, packageMeta }} />
const { container } = renderWithStore(
<ComponentToBeRendered contextValue={{ ...detailContextValue, packageMeta }} />,
store
);
expect(container.firstChild).toMatchSnapshot();
});
test('when there is a button to download a tarball', () => {
const { getByTitle } = render(
<ComponentToBeRendered contextValue={{ ...detailContextValue }} />
const { getByTitle } = renderWithStore(
<ComponentToBeRendered contextValue={{ ...detailContextValue }} />,
store
);
expect(getByTitle('Download tarball')).toBeTruthy();
});
test('when there is a button to open an issue', () => {
const { getByTitle } = render(
<ComponentToBeRendered contextValue={{ ...detailContextValue }} />
const { getByTitle } = renderWithStore(
<ComponentToBeRendered contextValue={{ ...detailContextValue }} />,
store
);
expect(getByTitle('Open an issue')).toBeTruthy();
});

@ -4,11 +4,11 @@ import DownloadIcon from '@material-ui/icons/CloudDownload';
import HomeIcon from '@material-ui/icons/Home';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { Theme } from 'verdaccio-ui/design-tokens/theme';
import { useAPI } from 'verdaccio-ui/providers/API/APIProvider';
import { extractFileName, downloadFile } from 'verdaccio-ui/utils/url';
import { Dispatch } from '../../store/store';
import FloatingActionButton from '../FloatingActionButton';
import Link from '../Link';
import Tooltip from '../Tooltip';
@ -34,13 +34,11 @@ export interface ActionBarActionProps {
/* eslint-disable react/jsx-no-bind */
const ActionBarAction: React.FC<ActionBarActionProps> = ({ type, link }) => {
const { t } = useTranslation();
const { getResource } = useAPI();
const dispatch = useDispatch<Dispatch>();
const handleDownload = useCallback(async () => {
const fileStream = await getResource(link);
const fileName = extractFileName(link);
downloadFile(fileStream, fileName);
}, [getResource, link]);
dispatch.download.getTarball({ link });
}, [dispatch, link]);
switch (type) {
case 'VISIT_HOMEPAGE':

@ -51,7 +51,6 @@ describe('<Author /> component', () => {
};
const wrapper = render(withAuthorComponent(packageMeta));
wrapper.debug();
expect(wrapper.queryAllByText('verdaccio')).toHaveLength(0);
});

@ -17,7 +17,7 @@ function loadTranslationFile(lng) {
return require(`./download_translations/${lng}/ui.json`);
} catch {
// eslint-disable-next-line no-console
console.error(`language ${lng} file not found, fallback to en-US`);
console.warn(`language ${lng} file not found, fallback to en-US`);
// in case the file is not there, fallback to en-US
return require(`./crowdin/ui.json`);
}

@ -1,27 +1,28 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import { Provider } from 'react-redux';
import APIProvider from 'verdaccio-ui/providers/API/APIProvider';
import AppConfigurationContext from 'verdaccio-ui/providers/config';
import App from './App';
import StyleBaseline from './design-tokens/StyleBaseline';
import ThemeProvider from './design-tokens/ThemeProvider';
import { store } from './store';
const rootNode = document.getElementById('root');
const renderApp = (Component: React.ElementType): void => {
ReactDOM.render(
<AppContainer>
<AppConfigurationContext>
<ThemeProvider>
<StyleBaseline />
<APIProvider>
<Provider store={store}>
<AppContainer>
<AppConfigurationContext>
<ThemeProvider>
<StyleBaseline />
<Component />
</APIProvider>
</ThemeProvider>
</AppConfigurationContext>
</AppContainer>,
</ThemeProvider>
</AppConfigurationContext>
</AppContainer>
</Provider>,
rootNode
);
};

@ -9,12 +9,6 @@ import UpLinks from './UpLinks';
describe('<UpLinks /> component', () => {
beforeEach(cleanup);
test('should return null without packageMeta', () => {
const wrapper = render(<UpLinks />);
wrapper.debug();
// expect(wrapper).toBeNull();
});
test('should render the component when there is no uplink', () => {
const packageMeta = {
latest: {

@ -1,84 +0,0 @@
import React from 'react';
import ActionBar from 'verdaccio-ui/components/ActionBar';
import { render, cleanup } from 'verdaccio-ui/utils/test-react-testing-library';
import { DetailContext } from '../../context';
import { DetailContextProps } from '../../version-config';
const detailContextValue: DetailContextProps = {
packageName: 'foo',
readMe: 'test',
enableLoading: () => {},
isLoading: false,
hasNotBeenFound: false,
packageMeta: {
_uplinks: {},
latest: {
name: 'verdaccio-ui/local-storage',
version: '8.0.1-next.1',
dist: {
fileCount: 0,
unpackedSize: 0,
tarball: 'http://localhost:8080/bootstrap/-/bootstrap-4.3.1.tgz',
},
homepage: 'https://verdaccio.org',
bugs: {
url: 'https://github.com/verdaccio/monorepo/issues',
},
},
},
};
const ComponentToBeRendered: React.FC<{ contextValue: DetailContextProps }> = ({
contextValue,
}) => (
<DetailContext.Provider value={contextValue}>
<ActionBar />
</DetailContext.Provider>
);
describe('<ActionBar /> component', () => {
afterEach(() => {
cleanup();
});
test('should render the component in default state', () => {
const { container } = render(<ComponentToBeRendered contextValue={detailContextValue} />);
expect(container.firstChild).toMatchSnapshot();
});
test('when there is no action bar data', () => {
const packageMeta = {
...detailContextValue.packageMeta,
latest: {
...detailContextValue.packageMeta.latest,
homepage: undefined,
bugs: undefined,
dist: {
...detailContextValue.packageMeta.latest.dist,
tarball: undefined,
},
},
};
const { container } = render(
<ComponentToBeRendered contextValue={{ ...detailContextValue, packageMeta }} />
);
expect(container.firstChild).toMatchSnapshot();
});
test('when there is a button to download a tarball', () => {
const { getByTitle } = render(
<ComponentToBeRendered contextValue={{ ...detailContextValue }} />
);
expect(getByTitle('Download tarball')).toBeTruthy();
});
test('when there is a button to open an issue', () => {
const { getByTitle } = render(
<ComponentToBeRendered contextValue={{ ...detailContextValue }} />
);
expect(getByTitle('Open an issue')).toBeTruthy();
});
});

@ -1,122 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ActionBar /> component should render the component in default state 1`] = `
.emotion-0 {
background-color: #4b5e40;
color: #fff;
margin-right: 10px;
}
.emotion-0:hover {
color: #4b5e40;
background: #fff;
}
<div
class="MuiBox-root MuiBox-root-1"
>
<a
class=""
href="https://verdaccio.org"
rel="noopener noreferrer"
target="_blank"
title="Visit homepage"
>
<span
class="MuiTypography-root MuiTypography-subtitle1"
>
<button
class="MuiButtonBase-root MuiFab-root emotion-0 emotion-1 MuiFab-sizeSmall"
data-testid="fab"
tabindex="0"
type="button"
>
<span
class="MuiFab-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</span>
</a>
<a
class=""
href="https://github.com/verdaccio/monorepo/issues"
rel="noopener noreferrer"
target="_blank"
title="Open an issue"
>
<span
class="MuiTypography-root MuiTypography-subtitle1"
>
<button
class="MuiButtonBase-root MuiFab-root emotion-0 emotion-1 MuiFab-sizeSmall"
data-testid="fab"
tabindex="0"
type="button"
>
<span
class="MuiFab-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 8h-2.81c-.45-.78-1.07-1.45-1.82-1.96L17 4.41 15.59 3l-2.17 2.17C12.96 5.06 12.49 5 12 5c-.49 0-.96.06-1.41.17L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6 8h-4v-2h4v2zm0-4h-4v-2h4v2z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</span>
</a>
<button
class="MuiButtonBase-root MuiFab-root emotion-0 emotion-1 MuiFab-sizeSmall"
data-testid="fab"
tabindex="0"
title="Download tarball"
type="button"
>
<span
class="MuiFab-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM17 13l-5 5-5-5h3V9h4v4h3z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div>
`;
exports[`<ActionBar /> component when there is no action bar data 1`] = `
<div
class="MuiBox-root MuiBox-root-2"
/>
`;

@ -1,20 +1,37 @@
import React, { useContext } from 'react';
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import Loading from 'verdaccio-ui/components/Loading';
import NotFound from 'verdaccio-ui/components/NotFound';
import { DetailContext } from './context';
import { Dispatch, RootState } from '../../store/store';
import getRouterPackageName from './get-route-package-name';
import VersionLayout from './VersionLayout';
interface Params {
scope?: string;
package: string;
version?: string;
}
const Version: React.FC = () => {
const detailContext = useContext(DetailContext);
const { isLoading, hasNotBeenFound } = detailContext;
const { version: packageVersion, package: pkgName, scope } = useParams<Params>();
const manifestStore = useSelector((state: RootState) => state.manifest);
const isLoading = useSelector((state: RootState) => state?.loading?.models.manifest);
const dispatch = useDispatch<Dispatch>();
useEffect(() => {
const packageName = getRouterPackageName(pkgName, scope);
dispatch.manifest.getManifest({ packageName, packageVersion });
}, [dispatch, pkgName, scope, packageVersion]);
if (isLoading) {
return <Loading />;
}
if (hasNotBeenFound) {
if (manifestStore.hasNotBeenFound) {
return <NotFound />;
}

@ -1,12 +1,11 @@
import React, { useEffect, useState, useCallback } from 'react';
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { PackageMetaInterface } from 'types/packageMeta';
import { useAPI } from 'verdaccio-ui/providers/API/APIProvider';
import { Dispatch, RootState } from '../../store/store';
import { DetailContext } from './context';
import getRouterPackageName from './get-route-package-name';
import isPackageVersionValid from './is-package-version-valid';
interface Params {
scope?: string;
@ -15,52 +14,23 @@ interface Params {
}
const VersionContextProvider: React.FC = ({ children }) => {
const { version, package: pkgName, scope } = useParams<Params>();
const [packageName, setPackageName] = useState(getRouterPackageName(pkgName, scope));
const [packageVersion, setPackageVersion] = useState(version);
const [packageMeta, setPackageMeta] = useState<PackageMetaInterface>();
const [readMe, setReadme] = useState<string>();
const [isLoading, setIsLoading] = useState<boolean>(true);
const [hasNotBeenFound, setHasNotBeenFound] = useState<boolean>();
const { callDetailPage, callReadme } = useAPI();
const { version: packageVersion, package: pkgName, scope } = useParams<Params>();
const { manifest, readme, packageName, hasNotBeenFound } = useSelector(
(state: RootState) => state.manifest
);
const isLoading = useSelector((state: RootState) => state?.loading?.models.manifest);
const dispatch = useDispatch<Dispatch>();
useEffect(() => {
const updatedPackageName = getRouterPackageName(pkgName, scope);
setPackageName(updatedPackageName);
}, [pkgName, scope]);
useEffect(() => {
setPackageVersion(version);
}, [version]);
const doCalls = useCallback(async () => {
try {
const packageMeta = await callDetailPage(packageName, packageVersion);
const readMe = await callReadme(packageName, packageVersion);
if (isPackageVersionValid(packageMeta, packageVersion)) {
setReadme(readMe);
setPackageMeta(packageMeta);
setIsLoading(false);
} else {
setIsLoading(false);
setHasNotBeenFound(true);
}
} catch (error: any) {
setHasNotBeenFound(true);
setIsLoading(false);
}
}, [packageName, packageVersion, callDetailPage, callReadme]);
useEffect(() => {
doCalls();
}, [doCalls]);
const packageName = getRouterPackageName(pkgName, scope);
dispatch.manifest.getManifest({ packageName, packageVersion });
}, [dispatch, packageVersion, pkgName, scope]);
return (
<DetailContext.Provider
value={{
packageMeta,
packageMeta: manifest,
packageVersion,
readMe,
readMe: readme,
packageName,
isLoading,
hasNotBeenFound,

@ -1,19 +0,0 @@
import { PackageMetaInterface } from '../../../types/packageMeta';
function isPackageVersionValid(
packageMeta: Partial<PackageMetaInterface>,
packageVersion?: string
): boolean {
if (!packageVersion || typeof packageVersion === 'undefined') {
// if is undefined, that means versions does not exist, we continue
return true;
}
if (packageMeta.versions) {
return Object.keys(packageMeta.versions).includes(packageVersion);
}
return false;
}
export default isPackageVersionValid;

@ -1,36 +1,19 @@
import React, { useEffect, useState, useCallback } from 'react';
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Loading from 'verdaccio-ui/components/Loading';
import { useAPI } from 'verdaccio-ui/providers/API/APIProvider';
import { Dispatch, RootState } from '../../store/store';
import { PackageList } from './PackageList';
interface Props {
isUserLoggedIn: boolean;
}
const Home: React.FC<Props> = ({ isUserLoggedIn }) => {
const [packages, setPackages] = useState([]);
const { getPackages } = useAPI();
const [isLoading, setIsLoading] = useState(true);
const loadPackages = useCallback(async () => {
try {
const packages = await getPackages();
// FIXME add correct type for package
setPackages(packages as never[]);
} catch (error: any) {
// FIXME: add dialog
// eslint-disable-next-line no-console
console.error({
title: 'Warning',
message: `Unable to load package list: ${error.message}`,
});
}
setIsLoading(false);
}, [getPackages]);
const Home: React.FC = () => {
const packages = useSelector((state: RootState) => state.packages.response);
const isLoading = useSelector((state: RootState) => state?.loading?.models.packages);
const dispatch = useDispatch<Dispatch>();
useEffect(() => {
loadPackages().then();
}, [isUserLoggedIn, loadPackages]);
dispatch.packages.getPackages();
}, [dispatch]);
return (
<div className="container content" data-testid="home-page-container">

@ -63,7 +63,7 @@ exports[`<Help /> component should load the component in default state 1`] = `
<span
class="emotion-6 emotion-7"
>
npm adduser --registry http://localhost
npm adduser --registry http://localhost:9000/
</span>
<button
class="MuiButtonBase-root MuiIconButton-root"
@ -102,7 +102,7 @@ exports[`<Help /> component should load the component in default state 1`] = `
<span
class="emotion-6 emotion-7"
>
npm publish --registry http://localhost
npm publish --registry http://localhost:9000/
</span>
<button
class="MuiButtonBase-root MuiIconButton-root"

@ -1,7 +1,9 @@
import React from 'react';
import { MemoryRouter } from 'react-router';
import { render, cleanup } from 'verdaccio-ui/utils/test-react-testing-library';
import { renderWithStore, cleanup } from 'verdaccio-ui/utils/test-react-testing-library';
import { store } from '../../../../store';
import Package from './Package';
@ -29,7 +31,7 @@ describe('<Package /> component', () => {
keywords: ['verdaccio'],
};
const wrapper = render(
const wrapper = renderWithStore(
<MemoryRouter>
<Package
author={props.author}
@ -39,7 +41,8 @@ describe('<Package /> component', () => {
time={props.time}
version={props.version}
/>
</MemoryRouter>
</MemoryRouter>,
store
);
// FUTURE: improve this expectectations

@ -5,6 +5,7 @@ import DownloadIcon from '@material-ui/icons/CloudDownload';
import HomeIcon from '@material-ui/icons/Home';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import Grid from 'verdaccio-ui/components/Grid';
import { Version, FileBinary, Time, Law } from 'verdaccio-ui/components/Icons';
@ -12,14 +13,12 @@ import Link from 'verdaccio-ui/components/Link';
import ListItem from 'verdaccio-ui/components/ListItem';
import Tooltip from 'verdaccio-ui/components/Tooltip';
import { Theme } from 'verdaccio-ui/design-tokens/theme';
import { useAPI } from 'verdaccio-ui/providers/API/APIProvider';
import { useConfig } from 'verdaccio-ui/providers/config';
import fileSizeSI from 'verdaccio-ui/utils/file-size';
import { formatDate, formatDateDistance, getAuthorName } from 'verdaccio-ui/utils/package';
import { extractFileName, downloadFile } from 'verdaccio-ui/utils/url';
import { isURL } from 'verdaccio-ui/utils/url';
import { PackageMetaInterface, Author as PackageAuthor } from '../../../../../types/packageMeta';
import { Dispatch, RootState } from '../../../../store/store';
import {
Author,
@ -72,19 +71,17 @@ const Package: React.FC<PackageInterface> = ({
time,
version,
}) => {
const config = useSelector((state: RootState) => state.configuration);
const dispatch = useDispatch<Dispatch>();
const { t } = useTranslation();
const { getResource } = useAPI();
const { configOptions } = useConfig();
const handleDownload = useCallback(
async (tarballDist: string) => {
// FIXME: check, the dist might be different thant npmjs
const link = tarballDist.replace(`https://registry.npmjs.org/`, configOptions.base as string);
const fileStream = await getResource(link);
const fileName = extractFileName(link);
downloadFile(fileStream, fileName);
const link = tarballDist.replace(`https://registry.npmjs.org/`, config.base);
dispatch.download.getTarball({ link });
},
[getResource, configOptions]
[dispatch, config]
);
const renderVersionInfo = (): React.ReactNode =>

@ -26,7 +26,6 @@ const PackageList: React.FC<Props> = ({ packages }) => {
packages[index];
// TODO: move format license to API side.
const formattedLicense = formatLicense(license);
return (
<CellMeasurer cache={cache} columnIndex={0} key={key} parent={parent} rowIndex={index}>
<div style={style}>
@ -56,21 +55,23 @@ const PackageList: React.FC<Props> = ({ packages }) => {
<WindowScroller>
{({ height, isScrolling, scrollTop, onChildScroll }) => (
<AutoSizer disableHeight={true}>
{({ width }) => (
<List
autoHeight={true}
deferredMeasurementCache={cache}
height={height}
isScrolling={isScrolling}
onScroll={onChildScroll}
overscanRowCount={3}
rowCount={packages.length}
rowHeight={cache.rowHeight}
rowRenderer={renderRow}
scrollTop={scrollTop}
width={width}
/>
)}
{({ width }) => {
return (
<List
autoHeight={true}
deferredMeasurementCache={cache}
height={height}
isScrolling={isScrolling}
onScroll={onChildScroll}
overscanRowCount={3}
rowCount={packages.length}
rowHeight={cache.rowHeight}
rowRenderer={renderRow}
scrollTop={scrollTop}
width={width}
/>
);
}}
</AutoSizer>
)}
</WindowScroller>

@ -1,7 +1,9 @@
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import { render, cleanup } from 'verdaccio-ui/utils/test-react-testing-library';
import { renderWithStore, cleanup } from 'verdaccio-ui/utils/test-react-testing-library';
import { store } from '../../../store';
import { PackageList } from './PackageList';
@ -12,7 +14,7 @@ describe('<PackageList /> component', () => {
const props = {
packages: [],
};
const wrapper = render(<PackageList packages={props.packages} />);
const wrapper = renderWithStore(<PackageList packages={props.packages} />, store);
expect(wrapper.getByText('No Package Published Yet.')).toBeInTheDocument();
});
@ -46,10 +48,11 @@ describe('<PackageList /> component', () => {
help: false,
};
const wrapper = render(
const wrapper = renderWithStore(
<BrowserRouter>
<PackageList packages={props.packages} />
</BrowserRouter>
</BrowserRouter>,
store
);
expect(wrapper.queryAllByTestId('package-item-list')).toHaveLength(3);

@ -1,109 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { Package } from '@verdaccio/types';
import React, { createContext, FunctionComponent, useContext, useMemo } from 'react';
import { useConfig } from 'verdaccio-ui/providers/config';
import { HEADERS } from '../../../lib/constants';
import { PackageMetaInterface } from '../../../types/packageMeta';
import API from './api';
type ConfigProviderProps = {
callReadme: (packageName: string, packageVersion?: string) => Promise<string>;
callDetailPage: (packageName: string, packageVersion?: string) => Promise<PackageMetaInterface>;
callSearch: (value: string, signal: AbortSignal) => Promise<string>;
getPackages: () => Promise<Package[]>;
doLogin: (username: string, password: string) => Promise<LoginBody>;
getResource: (link: string) => Promise<Blob>;
};
export interface LoginError {
type: string;
description: string;
}
export interface LoginBody {
username?: string;
token?: string;
error?: LoginError;
}
// @ts-ignore
const AppAPIContext = createContext<ConfigProviderProps>({});
const APIProvider: FunctionComponent = ({ children }) => {
const { configOptions } = useConfig();
const buildURL = (basePath: string) => {
return `${configOptions?.base}-/verdaccio/${basePath}`;
};
const callReadme = async (packageName: string, packageVersion?: string): Promise<string> => {
return await API.request<string>(
buildURL(`package/readme/${packageName}${packageVersion ? `?v=${packageVersion}` : ''}`),
'GET'
);
};
const callDetailPage = async (
packageName: string,
packageVersion?: string
): Promise<PackageMetaInterface> => {
return await API.request<PackageMetaInterface>(
buildURL(`sidebar/${packageName}${packageVersion ? `?v=${packageVersion}` : ''}`),
'GET'
);
};
const callSearch = async (value: string, signal: AbortSignal): Promise<string> => {
// https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API#Browser_compatibility
// FUTURE: signal is not well supported for IE and Samsung Browser
return API.request(buildURL(`search/${encodeURIComponent(value)}`), 'GET', {
signal,
headers: {},
});
};
const getPackages = async (): Promise<Package[]> => {
return await API.request(buildURL('packages'), 'GET');
};
const doLogin = async (username: string, password: string): Promise<LoginBody> => {
return await API.request(buildURL('login'), 'POST', {
body: JSON.stringify({ username, password }),
headers: {
Accept: HEADERS.JSON,
'Content-Type': HEADERS.JSON,
},
});
};
const getResource = async (link: string): Promise<Blob> => {
return await API.request(link, 'GET', {
headers: {
['accept']:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3',
},
credentials: 'include',
});
};
const value = useMemo(
() => ({
callReadme,
callDetailPage,
callSearch,
getPackages,
doLogin,
getResource,
}),
[callReadme, getResource, callDetailPage, callSearch, doLogin]
);
return <AppAPIContext.Provider value={value}>{children}</AppAPIContext.Provider>;
};
export default APIProvider;
export const useAPI = () => useContext(AppAPIContext);

@ -85,6 +85,7 @@ describe('api', () => {
credentials: 'same-origin',
headers: new Headers({
Authorization: 'Bearer token-xx-xx-xx',
'x-client': 'verdaccio-ui',
}),
method: 'GET',
});

@ -29,6 +29,8 @@ export function handleResponseType(response: Response): Promise<[boolean, any]>
return Promise.all([response.ok, response.text()]);
}
const AuthHeader = 'Authorization';
class API {
public request<T>(
url: string,
@ -38,11 +40,13 @@ class API {
const token = storage.getItem('token');
const headers = new Headers(options.headers);
if (token && headers.has('Authorization') === false) {
headers.set('Authorization', `Bearer ${token}`);
if (token && headers.has(AuthHeader) === false) {
headers.set(AuthHeader, `Bearer ${token}`);
options.headers = headers;
}
headers.set('x-client', 'verdaccio-ui');
return new Promise((resolve, reject) => {
fetch(url, {
method,

@ -1 +0,0 @@
export { default, useAPI } from './APIProvider';

@ -0,0 +1 @@
export { store } from './store';

@ -0,0 +1,42 @@
import { createModel } from '@rematch/core';
import { Package, TemplateUIOptions } from '@verdaccio/types';
import isEmpty from 'lodash/isEmpty';
import isNil from 'lodash/isNil';
import { PRIMARY_COLOR } from 'verdaccio-ui/utils/colors';
import API from '../../providers/API/api';
import type { RootModel } from '.';
const defaultValues: TemplateUIOptions = {
primaryColor: PRIMARY_COLOR,
darkMode: false,
pkgManagers: ['yarn', 'pnpm', 'npm'],
scope: '',
base: '',
login: true,
url_prefix: '',
title: 'Verdaccio',
};
function getConfiguration() {
const uiConfiguration = window?.__VERDACCIO_BASENAME_UI_OPTIONS ?? defaultValues;
if (isNil(uiConfiguration.primaryColor) || isEmpty(uiConfiguration.primaryColor)) {
uiConfiguration.primaryColor = PRIMARY_COLOR;
}
return uiConfiguration;
}
export const configuration = createModel<RootModel>()({
state: {
config: getConfiguration(),
},
effects: (dispatch) => ({
async getPackages() {
const payload: Package[] = await API.request(`/-/verdaccio/packages`, 'GET');
dispatch.packages.savePackages(payload);
},
}),
});

@ -0,0 +1,31 @@
import { createModel } from '@rematch/core';
import API from '../../providers/API/api';
import { downloadFile, extractFileName } from '../../utils/url';
import type { RootModel } from '.';
export const download = createModel<RootModel>()({
state: {},
reducers: {},
effects: () => ({
async getTarball({ link }) {
// const basePath = state.configuration.config.base;
try {
const fileStream: Blob = await API.request(link, 'GET', {
headers: {
['accept']:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3',
},
credentials: 'include',
});
const fileName = extractFileName(link);
downloadFile(fileStream, fileName);
} catch (error: any) {
// TODO: handle better error
// eslint-disable-next-line no-console
console.error('error on download', error);
}
},
}),
});

@ -0,0 +1,17 @@
import { Models } from '@rematch/core';
import { configuration } from './configuration';
import { download } from './download';
import { login } from './login';
import { manifest } from './manifest';
import { packages } from './packages';
import { search } from './search';
export interface RootModel extends Models<RootModel> {
packages: typeof packages;
manifest: typeof manifest;
configuration: typeof configuration;
download: typeof download;
login: typeof login;
search: typeof search;
}
export const models: RootModel = { packages, configuration, search, download, login, manifest };

@ -0,0 +1,90 @@
import { createModel } from '@rematch/core';
import i18next from 'i18next';
import { HEADERS } from '../../lib/constants';
import API from '../../providers/API/api';
import storage from '../../utils/storage';
import type { RootModel } from '.';
export type LoginError = {
type: string;
description: string;
};
export type LoginResponse = {
username: string | null;
token: string | null;
};
export type LoginBody = {
error?: LoginError;
} & LoginResponse;
const token = storage.getItem('token');
const username = storage.getItem('username');
const defaultUserState: LoginBody = {
token,
username,
};
export const login = createModel<RootModel>()({
state: {
username: defaultUserState.username,
token: defaultUserState.token,
},
reducers: {
logOutUser(state) {
storage.removeItem('username');
storage.removeItem('token');
return {
...state,
username: null,
token: null,
};
},
addError(state, error: LoginError) {
return {
...state,
error,
};
},
clearError(state) {
return {
...state,
error: undefined,
};
},
logInUser(state, response: LoginResponse) {
// we might persist this in another way with
storage.setItem('username', response.username as string);
storage.setItem('token', response.token as string);
return {
...state,
token: response.token as string,
username: response.username as string,
};
},
},
effects: (dispatch) => ({
async getUser({ username, password }, state) {
const basePath = state.configuration.config.base;
try {
const payload: LoginResponse = await API.request(`${basePath}-/verdaccio/login`, 'POST', {
body: JSON.stringify({ username, password }),
headers: {
Accept: HEADERS.JSON,
'Content-Type': HEADERS.JSON,
},
});
dispatch.login.logInUser(payload);
dispatch.packages.getPackages();
} catch (error: any) {
dispatch.login.addError({
type: 'error',
description: i18next.t('form-validation.unable-to-sign-in'),
});
}
},
}),
});

@ -0,0 +1,91 @@
import { createModel } from '@rematch/core';
import { Package } from '@verdaccio/types';
import { PackageMetaInterface } from 'types/packageMeta';
import API from '../../providers/API/api';
import type { RootModel } from '.';
function isPackageVersionValid(
packageMeta: Partial<PackageMetaInterface>,
packageVersion?: string
): boolean {
if (!packageVersion || typeof packageVersion === 'undefined') {
// if is undefined, that means versions does not exist, we continue
return true;
}
if (packageMeta.versions) {
return Object.keys(packageMeta.versions).includes(packageVersion);
}
return false;
}
export const manifest = createModel<RootModel>()({
state: {},
reducers: {
notFound(state) {
return {
...state,
hasNotBeenFound: true,
manifest: undefined,
packageName: undefined,
packageVersion: undefined,
readme: undefined,
};
},
clearError(state) {
return {
...state,
isError: null,
};
},
isError(state) {
return {
...state,
isError: true,
hasNotBeenFound: false,
manifest: undefined,
packageName: undefined,
packageVersion: undefined,
readme: undefined,
};
},
saveManifest(state, { packageName, packageVersion, manifest, readme }) {
return {
...state,
manifest,
packageName,
packageVersion,
readme,
hasNotBeenFound: false,
};
},
},
effects: (dispatch) => ({
async getManifest({ packageName, packageVersion }, state) {
const basePath = state.configuration.config.base;
try {
if (!isPackageVersionValid(packageName, packageVersion)) {
throw new Error('not found');
}
const manifest: Package = await API.request(
`${basePath}-/verdaccio/sidebar/${packageName}${
packageVersion ? `?v=${packageVersion}` : ''
}`
);
const readme: string = await API.request<string>(
`${basePath}-/verdaccio/package/readme/${packageName}${
packageVersion ? `?v=${packageVersion}` : ''
}`,
'GET'
);
dispatch.manifest.saveManifest({ packageName, packageVersion, manifest, readme });
} catch (error: any) {
dispatch.manifest.notFound();
}
},
}),
});

@ -0,0 +1,36 @@
import { createModel } from '@rematch/core';
import { Package } from '@verdaccio/types';
import API from '../../providers/API/api';
import type { RootModel } from '.';
export const packages = createModel<RootModel>()({
state: {
response: [] as Package[],
},
reducers: {
savePackages(state, response: Package[]) {
return {
...state,
response,
};
},
},
effects: (dispatch) => ({
async getPackages(_payload, state) {
const basePath = state.configuration.config.base;
try {
const payload: Package[] = await API.request(`${basePath}-/verdaccio/packages`, 'GET');
dispatch.packages.savePackages(payload);
} catch (error: any) {
// eslint-disable-next-line no-console
console.error({
title: 'Warning',
message: `Unable to load package list: ${error.message}`,
});
// TODO: handle error, display something retry or something
}
},
}),
});

@ -0,0 +1,81 @@
import { createModel } from '@rematch/core';
import { Package } from '@verdaccio/types';
import API from '../../providers/API/api';
import type { RootModel } from '.';
const CONSTANTS = {
API_DELAY: 300,
ABORT_ERROR: 'AbortError',
};
type SearchState = {
suggestions: Partial<Package>[];
controller: AbortController[];
};
export const search = createModel<RootModel>()({
state: {
suggestions: [],
controller: [],
} as SearchState,
reducers: {
clearRequestQueue(state) {
const controllers = state.controller;
controllers.forEach((request) => request.abort());
return {
...state,
controller: [],
};
},
addControllerToQueue(state, { controller }: { controller: AbortController }) {
const currentControllers = state.controller;
return {
...state,
controller: [...currentControllers, controller],
};
},
setError(state) {
return {
...state,
isError: true,
};
},
saveSearch(state, { suggestions }: { suggestions: Partial<Package>[] }) {
return {
...state,
suggestions,
isError: null,
};
},
},
effects: (dispatch) => ({
async getSuggestions({ value }, state) {
const basePath = state.configuration.config.base;
try {
const controller = new window.AbortController();
dispatch.search.addControllerToQueue({ controller });
const signal = controller.signal;
// https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API#Browser_compatibility
// FUTURE: signal is not well supported for IE and Samsung Browser
const suggestions: Partial<Package>[] = await API.request(
`${basePath}-/verdaccio/search/${encodeURIComponent(value)}`,
'GET',
{
signal,
headers: {},
}
);
dispatch.search.saveSearch({ suggestions });
} catch (error: any) {
if (error.name === CONSTANTS.ABORT_ERROR) {
dispatch.search.saveSearch({ suggestions: [] });
} else {
dispatch.search.setError();
}
}
},
}),
});

@ -0,0 +1,15 @@
import { init, RematchDispatch, RematchRootState } from '@rematch/core';
import loadingPlugin, { ExtraModelsFromLoading } from '@rematch/loading';
import { models, RootModel } from './models';
type FullModel = ExtraModelsFromLoading<RootModel>;
export const store = init<RootModel, FullModel>({
models,
plugins: [loadingPlugin()],
});
export type Store = typeof store;
export type Dispatch = RematchDispatch<RootModel>;
export type RootState = RematchRootState<RootModel, FullModel>;

@ -1,25 +1,38 @@
import { render } from '@testing-library/react';
import React from 'react';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import ThemeProvider from 'verdaccio-ui/design-tokens/ThemeProvider';
import APIProvider from 'verdaccio-ui/providers/API/APIProvider';
import AppConfigurationProvider from 'verdaccio-ui/providers/config';
import i18nConfig from '../i18n/config';
const renderWithStore = (ui, store) =>
render(ui, {
wrapper: ({ children }) => (
<Provider store={store}>
<AppConfigurationProvider>
<ThemeProvider>
<I18nextProvider i18n={i18nConfig}>{children}</I18nextProvider>
</ThemeProvider>
</AppConfigurationProvider>
</Provider>
),
});
const customRender = (node: React.ReactElement, ...options: any) => {
return render(
<AppConfigurationProvider>
<APIProvider>
<ThemeProvider>
<I18nextProvider i18n={i18nConfig}>{node}</I18nextProvider>
</ThemeProvider>
</APIProvider>
<ThemeProvider>
<I18nextProvider i18n={i18nConfig}>{node}</I18nextProvider>
</ThemeProvider>
</AppConfigurationProvider>,
...options
);
};
export * from '@testing-library/react';
// FIXME: rename all references with customRemder
export { customRender as render };
export { customRender, renderWithStore };

@ -18,7 +18,7 @@
"verdaccio-ui/utils/*": ["src/utils/*"]
}
},
"include": ["src", "types/*.d.ts", "src/i18n/**/*.json"],
"include": ["src", "types/*.d.ts", "src/i18n/**/*.json", "jest/unit/components"],
"references": [
{
"path": "../../node-api"

@ -1,26 +1,8 @@
// FIXME: this should comes from @verdaccio/types
type PackageManagers = 'pnpm' | 'yarn' | 'npm';
export interface VerdaccioOptions {
url_prefix: string;
base: string;
scope: string;
title: string;
primaryColor: string;
darkMode: boolean;
uri?: string;
login?: boolean;
language?: string;
version?: string;
protocol?: string;
host?: string;
logo?: string;
pkgManagers?: PackageManagers[];
}
import { TemplateUIOptions } from '@verdaccio/types';
declare global {
interface Window {
__VERDACCIO_BASENAME_UI_OPTIONS: VerdaccioOptions;
__VERDACCIO_BASENAME_UI_OPTIONS: TemplateUIOptions;
// FIXME: remove all these variables
VERDACCIO_PRIMARY_COLOR: string;
VERDACCIO_LOGO: string;
VERDACCIO_SCOPE: string;

@ -25,6 +25,7 @@ const debug = buildDebug('verdaccio:web:api:package');
function addPackageWebApi(route: Router, storage: Storage, auth: IAuth, config: Config): void {
const isLoginEnabled = config?.web?.login === true ?? true;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const anonymousRemoteUser: RemoteUser = {
name: undefined,
real_groups: [],
@ -35,9 +36,10 @@ function addPackageWebApi(route: Router, storage: Storage, auth: IAuth, config:
const checkAllow = (name: string, remoteUser: RemoteUser): Promise<boolean> =>
new Promise((resolve, reject): void => {
debug('is login disabled %o', isLoginEnabled);
const remoteUserAccess = !isLoginEnabled ? anonymousRemoteUser : remoteUser;
// FIXME: this logic does not work, review
// const remoteUserAccess = !isLoginEnabled ? anonymousRemoteUser : remoteUser;
try {
auth.allow_access({ packageName: name }, remoteUserAccess, (err, allowed): void => {
auth.allow_access({ packageName: name }, remoteUser, (err, allowed): void => {
if (err) {
resolve(false);
}

273
pnpm-lock.yaml generated

@ -74,7 +74,6 @@ importers:
jest-environment-jsdom: 27.1.0
jest-environment-jsdom-global: 3.0.0
jest-environment-node: 27.1.0
jest-fetch-mock: 3.0.3
jest-junit: 12.2.0
kleur: 3.0.3
lint-staged: 11.1.2
@ -165,7 +164,6 @@ importers:
jest-environment-jsdom: 27.1.0
jest-environment-jsdom-global: 3.0.0_jest-environment-jsdom@27.1.0
jest-environment-node: 27.1.0
jest-fetch-mock: 3.0.3
jest-junit: 12.2.0
kleur: 3.0.3
lint-staged: 11.1.2
@ -750,15 +748,18 @@ importers:
'@material-ui/core': 4.11.4
'@material-ui/icons': 4.11.2
'@material-ui/styles': 4.11.4
'@testing-library/dom': 8.2.0
'@rematch/core': 2.1.0
'@rematch/loading': 2.1.0
'@testing-library/dom': 8.5.0
'@testing-library/jest-dom': 5.14.1
'@testing-library/react': 12.0.0
'@testing-library/react': 12.1.0
'@types/react': 17.0.19
'@types/react-autosuggest': 10.1.5
'@types/react-dom': 17.0.9
'@types/react-helmet': 6.1.2
'@types/react-router-dom': 5.1.8
'@types/react-virtualized': 9.21.13
'@types/redux': 3.6.0
'@verdaccio/node-api': workspace:6.0.0-6-next.20
autosuggest-highlight: 3.1.1
babel-loader: 8.2.2
@ -782,6 +783,7 @@ importers:
localstorage-memory: 1.0.3
lodash: 4.17.21
mini-css-extract-plugin: 2.2.2
msw: 0.35.0
mutationobserver-shim: 0.3.7
node-mocks-http: 1.10.1
normalize.css: 8.0.1
@ -791,12 +793,14 @@ importers:
react: 17.0.2
react-autosuggest: 10.1.0
react-dom: 17.0.2
react-hook-form: 7.14.2
react-hook-form: 7.15.3
react-hot-loader: 4.13.0
react-i18next: 11.12.0
react-redux: 7.2.1
react-router: 5.2.1
react-router-dom: 5.3.0
react-virtualized: 9.22.3
redux: 4.1.1
rimraf: 3.0.2
standard-version: 9.3.1
style-loader: 3.2.1
@ -826,15 +830,18 @@ importers:
'@material-ui/core': 4.11.4_9c6a8df88c2691f81f37725d5b4de033
'@material-ui/icons': 4.11.2_842d6fd0a208aabbcab28b4283e0161f
'@material-ui/styles': 4.11.4_9c6a8df88c2691f81f37725d5b4de033
'@testing-library/dom': 8.2.0
'@rematch/core': 2.1.0_redux@4.1.1
'@rematch/loading': 2.1.0_@rematch+core@2.1.0
'@testing-library/dom': 8.5.0
'@testing-library/jest-dom': 5.14.1
'@testing-library/react': 12.0.0_react-dom@17.0.2+react@17.0.2
'@testing-library/react': 12.1.0_react-dom@17.0.2+react@17.0.2
'@types/react': 17.0.19
'@types/react-autosuggest': 10.1.5
'@types/react-dom': 17.0.9
'@types/react-helmet': 6.1.2
'@types/react-router-dom': 5.1.8
'@types/react-virtualized': 9.21.13
'@types/redux': 3.6.0
'@verdaccio/node-api': link:../../node-api
autosuggest-highlight: 3.1.1
babel-loader: 8.2.2_02ab79faf18a98050fd2cd956ffa58f7
@ -858,6 +865,7 @@ importers:
localstorage-memory: 1.0.3
lodash: 4.17.21
mini-css-extract-plugin: 2.2.2_webpack@5.52.0
msw: 0.35.0
mutationobserver-shim: 0.3.7
node-mocks-http: 1.10.1
normalize.css: 8.0.1
@ -867,12 +875,14 @@ importers:
react: 17.0.2
react-autosuggest: 10.1.0_react@17.0.2
react-dom: 17.0.2_react@17.0.2
react-hook-form: 7.14.2_react@17.0.2
react-hook-form: 7.15.3_react@17.0.2
react-hot-loader: 4.13.0_9c6a8df88c2691f81f37725d5b4de033
react-i18next: 11.12.0_i18next@20.6.1+react@17.0.2
react-redux: 7.2.1_e084f946920a04d85390aa355643c4d5
react-router: 5.2.1_react@17.0.2
react-router-dom: 5.3.0_react@17.0.2
react-virtualized: 9.22.3_react-dom@17.0.2+react@17.0.2
redux: 4.1.1
rimraf: 3.0.2
standard-version: 9.3.1
style-loader: 3.2.1_webpack@5.52.0
@ -5713,6 +5723,26 @@ packages:
resolution: {integrity: sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA==}
dev: false
/@mswjs/cookies/0.1.6:
resolution: {integrity: sha512-A53XD5TOfwhpqAmwKdPtg1dva5wrng2gH5xMvklzbd9WLTSVU953eCRa8rtrrm6G7Cy60BOGsBRN89YQK0mlKA==}
dependencies:
'@types/set-cookie-parser': 2.4.1
set-cookie-parser: 2.4.8
dev: true
/@mswjs/interceptors/0.12.7:
resolution: {integrity: sha512-eGjZ3JRAt0Fzi5FgXiV/P3bJGj0NqsN7vBS0J0FO2AQRQ0jCKQS4lEFm4wvlSgKQNfeuc/Vz6d81VtU3Gkx/zg==}
dependencies:
'@open-draft/until': 1.0.3
'@xmldom/xmldom': 0.7.5
debug: 4.3.2
headers-utils: 3.0.2
outvariant: 1.2.1
strict-event-emitter: 0.2.0
transitivePeerDependencies:
- supports-color
dev: true
/@nicolo-ribaudo/chokidar-2/2.1.8-no-fsevents.2:
resolution: {integrity: sha512-Fb8WxUFOBQVl+CX4MWet5o7eCc6Pj04rXIwVKZ6h1NnqTo45eOQW6aWyhG25NIODvWFwTDMwBsYxrQ3imxpetg==}
requiresBuild: true
@ -5749,6 +5779,10 @@ packages:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.11.0
/@open-draft/until/1.0.3:
resolution: {integrity: sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==}
dev: true
/@polka/url/1.0.0-next.15:
resolution: {integrity: sha512-15spi3V28QdevleWBNXE4pIls3nFZmBbUGrW9IVPwiQczuSb9n76TCB4bsk8TSel+I1OkHEdPhu5QKMfY6rQHA==}
@ -5795,6 +5829,23 @@ packages:
resolution: {integrity: sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=}
dev: false
/@rematch/core/2.1.0_redux@4.1.1:
resolution: {integrity: sha512-izr4LlXsHp1gfK8v05FNvqSthX64cCj59/x3tu+3qNXSuSM7d1/YXAlUtBJIoX9RApr+d1mqA7CCAteUjZrdLg==}
engines: {node: '>=10'}
peerDependencies:
redux: '>=4'
dependencies:
redux: 4.1.1
dev: true
/@rematch/loading/2.1.0_@rematch+core@2.1.0:
resolution: {integrity: sha512-4sLWHiuTW50zjdbhUVHb9HB9Ors6+ktM9tANBOinYu4c43TSArZLkaC7lb8fZYLb6eCsHHAtrrWYsz8lBJ6UOw==}
peerDependencies:
'@rematch/core': '>=2'
dependencies:
'@rematch/core': 2.1.0_redux@4.1.1
dev: true
/@sideway/address/4.1.0:
resolution: {integrity: sha512-wAH/JYRXeIFQRsxerIuLjgUu2Xszam+O5xKeatJ4oudShOOirfmsQ1D6LL54XOU2tizpCYku+s1wmU0SYdpoSA==}
dependencies:
@ -5974,8 +6025,8 @@ packages:
dependencies:
defer-to-connect: 1.1.3
/@testing-library/dom/8.2.0:
resolution: {integrity: sha512-U8cTWENQPHO3QHvxBdfltJ+wC78ytMdg69ASvIdkGdQ/XRg4M9H2vvM3mHddxl+w/fM6NNqzGMwpQoh82v9VIA==}
/@testing-library/dom/8.5.0:
resolution: {integrity: sha512-O0fmHFaPlqaYCpa/cBL0cvroMridb9vZsMLacgIqrlxj+fd+bGF8UfAgwsLCHRF84KLBafWlm9CuOvxeNTlodw==}
engines: {node: '>=12'}
dependencies:
'@babel/code-frame': 7.14.5
@ -5985,7 +6036,7 @@ packages:
chalk: 4.1.1
dom-accessibility-api: 0.5.6
lz-string: 1.4.4
pretty-format: 27.0.2
pretty-format: 27.1.0
dev: true
/@testing-library/jest-dom/5.14.1:
@ -6003,15 +6054,15 @@ packages:
redent: 3.0.0
dev: true
/@testing-library/react/12.0.0_react-dom@17.0.2+react@17.0.2:
resolution: {integrity: sha512-sh3jhFgEshFyJ/0IxGltRhwZv2kFKfJ3fN1vTZ6hhMXzz9ZbbcTgmDYM4e+zJv+oiVKKEWZPyqPAh4MQBI65gA==}
/@testing-library/react/12.1.0_react-dom@17.0.2+react@17.0.2:
resolution: {integrity: sha512-Ge3Ht3qXE82Yv9lyPpQ7ZWgzo/HgOcHu569Y4ZGWcZME38iOFiOg87qnu6hTEa8jTJVL7zYovnvD3GE2nsNIoQ==}
engines: {node: '>=12'}
peerDependencies:
react: '*'
react-dom: '*'
dependencies:
'@babel/runtime': 7.15.4
'@testing-library/dom': 8.2.0
'@testing-library/dom': 8.5.0
react: 17.0.2
react-dom: 17.0.2_react@17.0.2
dev: true
@ -6118,6 +6169,10 @@ packages:
'@types/node': 16.9.1
dev: true
/@types/cookie/0.4.1:
resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==}
dev: true
/@types/cookiejar/2.1.2:
resolution: {integrity: sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==}
dev: true
@ -6190,6 +6245,13 @@ packages:
resolution: {integrity: sha512-e+2rjEwK6KDaNOm5Aa9wNGgyS9oSZU/4pfSMMPYNOfjvFI0WVXm29+ITRFr6aKDvvKo7uU1jV68MW4ScsfDi7Q==}
dev: true
/@types/inquirer/7.3.3:
resolution: {integrity: sha512-HhxyLejTHMfohAuhRun4csWigAMjXTmRyiJTU1Y/I1xmggikFMkOUoMQRlFm+zQcPEGHSs3io/0FAmNZf8EymQ==}
dependencies:
'@types/through': 0.0.30
rxjs: 6.6.7
dev: true
/@types/istanbul-lib-coverage/2.0.3:
resolution: {integrity: sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==}
dev: true
@ -6226,6 +6288,10 @@ packages:
pretty-format: 27.0.2
dev: true
/@types/js-levenshtein/1.1.0:
resolution: {integrity: sha512-14t0v1ICYRtRVcHASzes0v/O+TIeASb8aD55cWF1PidtInhFWSXcmhzhHqGjUWf9SUq1w70cvd1cWKUULubAfQ==}
dev: true
/@types/json-schema/7.0.6:
resolution: {integrity: sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==}
@ -6432,6 +6498,13 @@ packages:
csstype: 3.0.8
dev: true
/@types/redux/3.6.0:
resolution: {integrity: sha1-8evh5UEVGAcuT9/KXHbhbnTBOZo=}
deprecated: This is a stub types definition for Redux (https://github.com/reactjs/redux). Redux provides its own type definitions, so you don't need @types/redux installed!
dependencies:
redux: 4.1.1
dev: true
/@types/request/2.48.7:
resolution: {integrity: sha512-GWP9AZW7foLd4YQxyFZDBepl0lPsWLMEXDZUjQ/c1gqVPDPECrRZyEzuhJdnPWioFCq3Tv0qoGpMD6U+ygd4ZA==}
dependencies:
@ -6466,6 +6539,12 @@ packages:
'@types/node': 16.9.1
dev: true
/@types/set-cookie-parser/2.4.1:
resolution: {integrity: sha512-N0IWe4vT1w5IOYdN9c9PNpQniHS+qe25W4tj4vfhJDJ9OkvA/YA55YUhaC+HNmMMeLlOSnBW9UMno0qlt5xu3Q==}
dependencies:
'@types/node': 16.9.1
dev: true
/@types/sonic-boom/2.1.1:
resolution: {integrity: sha512-CiKn+8CDgtBspfAVPwC8PXCMPhqeL7pFS4aWuj+WJnHLZlu4OGPctdZ6Mob43jRe0kkd7Ztb2Hcu9kzB+b7ZFw==}
deprecated: This is a stub types definition. sonic-boom provides its own type definitions, so you do not need this installed.
@ -6509,6 +6588,12 @@ packages:
'@types/jest': 26.0.19
dev: true
/@types/through/0.0.30:
resolution: {integrity: sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg==}
dependencies:
'@types/node': 16.9.1
dev: true
/@types/tough-cookie/4.0.0:
resolution: {integrity: sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==}
dev: true
@ -7032,6 +7117,11 @@ packages:
webpack-cli: 4.8.0_3691794a826a95bb278217ad314163a5
dev: true
/@xmldom/xmldom/0.7.5:
resolution: {integrity: sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A==}
engines: {node: '>=10.0.0'}
dev: true
/@xtuc/ieee754/1.2.0:
resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
@ -8547,6 +8637,11 @@ packages:
string-width: 4.2.2
dev: true
/cli-width/3.0.0:
resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==}
engines: {node: '>= 10'}
dev: true
/clipanion/3.0.1:
resolution: {integrity: sha512-/ujK3YJ1MGjGr18w99Gl9XZjy4xcC/5bZRJXsgvYG6GbUTO4CTKriC+oUxDbo8G+G/dxDqSJhm8QIDnK6iH6Ig==}
dependencies:
@ -9018,6 +9113,11 @@ packages:
resolution: {integrity: sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==}
engines: {node: '>= 0.6'}
/cookie/0.4.1:
resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==}
engines: {node: '>= 0.6'}
dev: true
/cookiejar/2.1.2:
resolution: {integrity: sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==}
@ -9169,6 +9269,7 @@ packages:
resolution: {integrity: sha512-KBPUbqgFjzWlVcURG+Svp9TlhA5uliYtiNx/0r8nv0pdypeQCRJ9IaSIc3q/x3q8t3F75cHuwxVql1HFGHCNJQ==}
dependencies:
node-fetch: 2.6.1
dev: false
/cross-spawn/5.1.0:
resolution: {integrity: sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=}
@ -10732,6 +10833,11 @@ packages:
resolution: {integrity: sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==}
engines: {node: '>=0.8.x'}
/events/3.3.0:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
dev: true
/eventsource/1.0.7:
resolution: {integrity: sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==}
engines: {node: '>=0.12.0'}
@ -11874,6 +11980,11 @@ packages:
resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==}
dev: true
/graphql/15.5.3:
resolution: {integrity: sha512-sM+jXaO5KinTui6lbK/7b7H/Knj9BpjGxZ+Ki35v7YbUJxxdBCUqNM0h3CRVU1ZF9t5lNiBzvBCSYPvIwxPOQA==}
engines: {node: '>= 10.x'}
dev: true
/gray-matter/4.0.3:
resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==}
engines: {node: '>=6.0'}
@ -12144,6 +12255,10 @@ packages:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
/headers-utils/3.0.2:
resolution: {integrity: sha512-xAxZkM1dRyGV2Ou5bzMxBPNLoRCjcX+ya7KSWybQD2KwLphxsapUVK6x/02o7f4VU6GPSXch9vNY2+gkU8tYWQ==}
dev: true
/hex-color-regex/1.1.0:
resolution: {integrity: sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==}
@ -12594,6 +12709,26 @@ packages:
resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==}
dev: false
/inquirer/8.1.5:
resolution: {integrity: sha512-G6/9xUqmt/r+UvufSyrPpt84NYwhKZ9jLsgMbQzlx804XErNupor8WQdBnBRrXmBfTPpuwf1sV+ss2ovjgdXIg==}
engines: {node: '>=8.0.0'}
dependencies:
ansi-escapes: 4.3.2
chalk: 4.1.1
cli-cursor: 3.1.0
cli-width: 3.0.0
external-editor: 3.1.0
figures: 3.2.0
lodash: 4.17.21
mute-stream: 0.0.8
ora: 5.4.1
run-async: 2.4.1
rxjs: 7.3.0
string-width: 4.2.2
strip-ansi: 6.0.0
through: 2.3.8
dev: true
/internal-ip/4.3.0:
resolution: {integrity: sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==}
engines: {node: '>=6'}
@ -12864,6 +12999,10 @@ packages:
resolution: {integrity: sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==}
engines: {node: '>= 0.4'}
/is-node-process/1.0.1:
resolution: {integrity: sha512-5IcdXuf++TTNt3oGl9EBdkvndXA8gmc4bz/Y+mdEpWh3Mcn/+kOw6hI7LD5CocqJWMzeb0I0ClndRVNdEPuJXQ==}
dev: true
/is-npm/4.0.0:
resolution: {integrity: sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==}
engines: {node: '>=8'}
@ -13300,13 +13439,6 @@ packages:
jest-util: 27.1.0
dev: true
/jest-fetch-mock/3.0.3:
resolution: {integrity: sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==}
dependencies:
cross-fetch: 3.0.6
promise-polyfill: 8.2.0
dev: true
/jest-get-type/26.3.0:
resolution: {integrity: sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==}
engines: {node: '>= 10.14.2'}
@ -13677,6 +13809,11 @@ packages:
resolution: {integrity: sha512-Frdq2+tRRGLQUIQOgsIGSCd1VePCS2fsddTG5dTCqR0JHgltXWfsxnY0gIXPoMeRmdom6Oyq+UMOFg5suduOjQ==}
dev: true
/js-levenshtein/1.1.6:
resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==}
engines: {node: '>=0.10.0'}
dev: true
/js-tokens/4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@ -15028,6 +15165,35 @@ packages:
/ms/2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
/msw/0.35.0:
resolution: {integrity: sha512-V7A6PqaS31F1k//fPS0OnO7vllfaqBUFsMEu3IpYixyWpiUInfyglodnbXhhtDyytkQikpkPZv8TZi/CvZzv/w==}
hasBin: true
requiresBuild: true
dependencies:
'@mswjs/cookies': 0.1.6
'@mswjs/interceptors': 0.12.7
'@open-draft/until': 1.0.3
'@types/cookie': 0.4.1
'@types/inquirer': 7.3.3
'@types/js-levenshtein': 1.1.0
chalk: 4.1.1
chokidar: 3.5.1
cookie: 0.4.1
graphql: 15.5.3
headers-utils: 3.0.2
inquirer: 8.1.5
is-node-process: 1.0.1
js-levenshtein: 1.1.6
node-fetch: 2.6.1
node-match-path: 0.6.3
statuses: 2.0.1
strict-event-emitter: 0.2.0
type-fest: 1.4.0
yargs: 17.1.1
transitivePeerDependencies:
- supports-color
dev: true
/multicast-dns-service-types/1.1.0:
resolution: {integrity: sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=}
@ -15042,6 +15208,10 @@ packages:
resolution: {integrity: sha512-oRIDTyZQU96nAiz2AQyngwx1e89iApl2hN5AOYwyxLUB47UYsU3Wv9lJWqH5y/QdiYkc5HQLi23ZNB3fELdHcQ==}
dev: true
/mute-stream/0.0.8:
resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==}
dev: true
/mv/2.1.1:
resolution: {integrity: sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=}
engines: {node: '>=0.8.0'}
@ -15195,6 +15365,10 @@ packages:
resolution: {integrity: sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=}
dev: true
/node-match-path/0.6.3:
resolution: {integrity: sha512-fB1reOHKLRZCJMAka28hIxCwQLxGmd7WewOCBDYKpyA1KXi68A7vaGgdZAPhY2E6SXoYt3KqYCCvXLJ+O0Fu/Q==}
dev: true
/node-mocks-http/1.10.1:
resolution: {integrity: sha512-/Nz83kiJ3z+vGqxmlDyv8+L1CJno+gH23DzG3oPH9dBSfMYa5IFVwPgZpXCB2kdiiIu/HoDpZ2BuLqQs7qjFLQ==}
engines: {node: '>=0.6'}
@ -15679,6 +15853,10 @@ packages:
resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==}
dev: true
/outvariant/1.2.1:
resolution: {integrity: sha512-bcILvFkvpMXh66+Ubax/inxbKRyWTUiiFIW2DWkiS79wakrLGn3Ydy+GvukadiyfZjaL6C7YhIem4EZSM282wA==}
dev: true
/p-cancelable/1.1.0:
resolution: {integrity: sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==}
engines: {node: '>=6'}
@ -16824,10 +17002,6 @@ packages:
engines: {node: '>=0.4.0'}
dev: true
/promise-polyfill/8.2.0:
resolution: {integrity: sha512-k/TC0mIcPVF6yHhUvwAp7cvL6I2fFV7TzF1DuGPI8mBh4QQazf36xCKEHKTZKRysEoTQoQdKyP25J8MPJp7j5g==}
dev: true
/promise/7.3.1:
resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==}
dependencies:
@ -17134,8 +17308,8 @@ packages:
react-side-effect: 2.1.1_react@17.0.2
dev: false
/react-hook-form/7.14.2_react@17.0.2:
resolution: {integrity: sha512-32uvgKkaE/0vOncfnJdwQhfahhocPpcb5c7F4j9Eq7dOnqS2Hg8h70Bmt6KXb6veLSWJultc1+ik9QSfqXFmLA==}
/react-hook-form/7.15.3_react@17.0.2:
resolution: {integrity: sha512-z30aZoEHkWE8oZvad4OcYSBI0kQua/T5sFGH9tB2HfeykFnP/pGXNap8lDio4/U1yPj2ffpbvRIvqKd/6jjBVA==}
peerDependencies:
react: ^16.8.0 || ^17
dependencies:
@ -17241,6 +17415,29 @@ packages:
react: 17.0.2
dev: false
/react-redux/7.2.1_e084f946920a04d85390aa355643c4d5:
resolution: {integrity: sha512-T+VfD/bvgGTUA74iW9d2i5THrDQWbweXP0AVNI8tNd1Rk5ch1rnMiJkDD67ejw7YBKM4+REvcvqRuWJb7BLuEg==}
peerDependencies:
react: ^16.8.3
react-dom: '*'
react-native: '*'
redux: ^2.0.0 || ^3.0.0 || ^4.0.0-0
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
dependencies:
'@babel/runtime': 7.15.4
hoist-non-react-statics: 3.3.2
loose-envify: 1.4.0
prop-types: 15.7.2
react: 17.0.2
react-dom: 17.0.2_react@17.0.2
react-is: 16.13.1
redux: 4.1.1
dev: true
/react-router-config/5.1.1_react-router@5.2.0+react@17.0.2:
resolution: {integrity: sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==}
peerDependencies:
@ -17514,6 +17711,12 @@ packages:
strip-indent: 3.0.0
dev: true
/redux/4.1.1:
resolution: {integrity: sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw==}
dependencies:
'@babel/runtime': 7.15.4
dev: true
/regenerate-unicode-properties/8.2.0:
resolution: {integrity: sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==}
engines: {node: '>=4'}
@ -17986,6 +18189,11 @@ packages:
strip-json-comments: 3.1.1
dev: false
/run-async/2.4.1:
resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==}
engines: {node: '>=0.12.0'}
dev: true
/run-parallel/1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
dependencies:
@ -18699,6 +18907,11 @@ packages:
resolution: {integrity: sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=}
engines: {node: '>= 0.6'}
/statuses/2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'}
dev: true
/std-env/2.3.0:
resolution: {integrity: sha512-4qT5B45+Kjef2Z6pE0BkskzsH0GO7GrND0wGlTM1ioUe3v0dGYx9ZJH0Aro/YyA8fqQ5EyIKDRjZojJYMFTflw==}
dependencies:
@ -18730,6 +18943,12 @@ packages:
mixme: 0.4.0
dev: true
/strict-event-emitter/0.2.0:
resolution: {integrity: sha512-zv7K2egoKwkQkZGEaH8m+i2D0XiKzx5jNsiSul6ja2IYFvil10A59Z9Y7PPAAe5OW53dQUf9CfsHKzjZzKkm1w==}
dependencies:
events: 3.3.0
dev: true
/string-argv/0.3.1:
resolution: {integrity: sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==}
engines: {node: '>=0.6.19'}

@ -103,7 +103,7 @@ describe('/ (Verdaccio Page)', () => {
const accountButton = await page.$('#header--button-account');
expect(accountButton).toBeDefined();
// check whether user is logged
const buttonLogout = await page.$('#header--button-logout');
const buttonLogout = await page.$('#logOutDialogIcon');
expect(buttonLogout).toBeDefined();
});
@ -113,7 +113,7 @@ describe('/ (Verdaccio Page)', () => {
// we assume the user is logged already
await clickElement('#header--button-account', { delay: 500 });
await page.waitForTimeout(1000);
await clickElement('#header--button-logout > span', { delay: 500 });
await clickElement('#logOutDialogIcon > span', { delay: 500 });
await page.waitForTimeout(1000);
await evaluateSignIn();
});