1
0
mirror of https://github.com/verdaccio/verdaccio.git synced 2024-12-24 21:15:51 +01:00

feat: upgrade react 18 (#3495)

* chore: update react 18

* Create four-ways-try.md

* Update signin.cy.ts

* chore: new ci

* Update e2e-ui.yml

* Update e2e-ui.yml

* ci

* ci

* ci

* Update e2e-ui.yml

* Update e2e-ui.yml

* chore: fix ui test

* Update publish.cy.ts

* chore: update tests

* add strict mode
This commit is contained in:
Juan Picado 2022-11-12 22:05:08 +01:00 committed by GitHub
parent 17984fa31b
commit 0481b9a329
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 566 additions and 572 deletions

@ -0,0 +1,5 @@
---
'@verdaccio/ui-theme': minor
---
feat: upgrade to react 18

@ -33,7 +33,9 @@ jobs:
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
- name: Install pnpm - name: Install pnpm
run: npm i pnpm@6.32.15 -g run: |
corepack enable
corepack prepare --activate pnpm@6.32.15
- name: set store - name: set store
run: | run: |
mkdir ~/.pnpm-store mkdir ~/.pnpm-store
@ -58,7 +60,9 @@ jobs:
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
- name: Install pnpm - name: Install pnpm
run: npm i pnpm@6.32.15 -g run: |
corepack enable
corepack prepare --activate pnpm@6.32.15
- uses: actions/cache@1c73980b09e7aea7201f325a7aa3ad00beddcdda # tag=v3 - uses: actions/cache@1c73980b09e7aea7201f325a7aa3ad00beddcdda # tag=v3
with: with:
path: ~/.pnpm-store path: ~/.pnpm-store
@ -78,7 +82,9 @@ jobs:
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
- name: Install pnpm - name: Install pnpm
run: npm i pnpm@6.32.15 -g run: |
corepack enable
corepack prepare --activate pnpm@6.32.15
- uses: actions/cache@1c73980b09e7aea7201f325a7aa3ad00beddcdda # tag=v3 - uses: actions/cache@1c73980b09e7aea7201f325a7aa3ad00beddcdda # tag=v3
with: with:
path: ~/.pnpm-store path: ~/.pnpm-store
@ -87,7 +93,7 @@ jobs:
run: pnpm recursive install --offline --frozen-lockfile --reporter=silence --ignore-scripts run: pnpm recursive install --offline --frozen-lockfile --reporter=silence --ignore-scripts
- name: Lint - name: Lint
run: pnpm format:check run: pnpm format:check
build: test:
needs: [format, lint] needs: [format, lint]
strategy: strategy:
fail-fast: true fail-fast: true
@ -103,7 +109,9 @@ jobs:
with: with:
node-version: ${{ matrix.node_version }} node-version: ${{ matrix.node_version }}
- name: Install pnpm - name: Install pnpm
run: npm i pnpm@6.32.15 -g run: |
corepack enable
corepack prepare --activate pnpm@6.32.15
- uses: actions/cache@1c73980b09e7aea7201f325a7aa3ad00beddcdda # tag=v3 - uses: actions/cache@1c73980b09e7aea7201f325a7aa3ad00beddcdda # tag=v3
with: with:
path: ~/.pnpm-store path: ~/.pnpm-store
@ -114,31 +122,8 @@ jobs:
run: pnpm build run: pnpm build
- name: Test - name: Test
run: pnpm test run: pnpm test
ci-e2e-ui:
needs: [format, lint]
runs-on: ubuntu-latest
name: UI Test E2E
steps:
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
- uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # tag=v3
with:
node-version-file: '.nvmrc'
- name: Install pnpm
run: npm i pnpm@6.32.15 -g
- uses: actions/cache@1c73980b09e7aea7201f325a7aa3ad00beddcdda # tag=v3
with:
path: ~/.pnpm-store
key: pnpm-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install
run: pnpm recursive install --offline --frozen-lockfile --reporter=silence --registry http://localhost:4873
- name: build
run: pnpm build
- name: Test UI
run: pnpm test:e2e:ui
# env:
# DEBUG: verdaccio:e2e*
sync-translations: sync-translations:
needs: [ci-e2e-ui] needs: [test]
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: synchronize translations name: synchronize translations
if: (github.event_name == 'push' && github.ref == 'refs/heads/master') || github.event_name == 'workflow_dispatch' if: (github.event_name == 'push' && github.ref == 'refs/heads/master') || github.event_name == 'workflow_dispatch'
@ -148,7 +133,9 @@ jobs:
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
- name: Install pnpm - name: Install pnpm
run: npm i pnpm@6.32.15 -g run: |
corepack enable
corepack prepare --activate pnpm@6.32.15
- uses: actions/cache@1c73980b09e7aea7201f325a7aa3ad00beddcdda # tag=v3 - uses: actions/cache@1c73980b09e7aea7201f325a7aa3ad00beddcdda # tag=v3
with: with:
path: ~/.pnpm-store path: ~/.pnpm-store

36
.github/workflows/e2e-ui.yml vendored Normal file

@ -0,0 +1,36 @@
name: E2E UI
on: [pull_request]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
name: UI Test E2E
services:
verdaccio:
image: verdaccio/verdaccio:5
ports:
- 4873:4873
env:
NODE_ENV: production
steps:
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
- name: Use Node
uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # tag=v3
with:
node-version-file: '.nvmrc'
- name: Install pnpm
run: |
corepack enable
corepack prepare --activate pnpm@6.32.15
- name: Install
run: pnpm install --frozen-lockfile --reporter=silence --registry http://localhost:4873
- name: build
run: pnpm build
- name: Test UI
run: pnpm test:e2e:ui
- uses: actions/upload-artifact@v3
with:
name: videos
path: /home/runner/work/verdaccio/verdaccio/e2e/ui/cypress/videos

1
.npmrc

@ -1,5 +1,4 @@
always-auth = true always-auth = true
recursive-install = true recursive-install = true
registry = https://registry.verdaccio.org
loglevel=info loglevel=info
fetch-retries="10" fetch-retries="10"

@ -3,18 +3,26 @@ web:
title: verdaccio-server-e2e title: verdaccio-server-e2e
login: true login: true
log: { type: stdout, format: pretty, level: debug } log: { type: stdout, format: json, level: info }
uplinks:
npmjs:
url: https://registry.npmjs.org/
auth: auth:
htpasswd: htpasswd:
file: ./htpasswd file: ./htpasswd
packages: packages:
'@verdaccio/*':
access: $all
publish: $authenticated
'@*/*': '@*/*':
access: $all access: $all
publish: $authenticated publish: $authenticated
proxy: npmjs
'**': '**':
access: $all access: $all
publish: $authenticated publish: $authenticated
proxy: npmjs
_debug: true _debug: true

@ -29,8 +29,8 @@ export default defineConfig({
}); });
on('task', { on('task', {
publishScoped() { publishScoped({ pkgName }) {
const scopedPackageMetadata = generatePackageMetadata('pkg-scoped', '1.0.6'); const scopedPackageMetadata = generatePackageMetadata(pkgName, '1.0.6');
const server = new ServerQuery(registry1.getRegistryUrl()); const server = new ServerQuery(registry1.getRegistryUrl());
server server
.putPackage(scopedPackageMetadata.name, scopedPackageMetadata, { .putPackage(scopedPackageMetadata.name, scopedPackageMetadata, {

@ -5,18 +5,19 @@ describe('publish spec', () => {
// @ts-expect-error // @ts-expect-error
const registry = await cy.task('registry'); const registry = await cy.task('registry');
ctx.url = registry.registryUrl; ctx.url = registry.registryUrl;
const pkgName = `@verdaccio/pkg-scoped`;
cy.intercept('POST', '/-/verdaccio/sec/login').as('sign'); cy.intercept('POST', '/-/verdaccio/sec/login').as('sign');
cy.intercept('GET', '/-/verdaccio/data/packages').as('pkgs'); cy.intercept('GET', '/-/verdaccio/data/packages').as('pkgs');
cy.intercept('GET', '/-/verdaccio/data/sidebar/pkg-scoped').as('sidebar'); cy.intercept('GET', `/-/verdaccio/data/sidebar/${pkgName}`).as('sidebar');
cy.intercept('GET', '/-/verdaccio/data/package/readme/pkg-scoped').as('readme'); cy.intercept('GET', `/-/verdaccio/data/package/readme/${pkgName}`).as('readme');
cy.task('publishScoped', { pkgName: 'pkg-protected' }); cy.task('publishScoped', { pkgName });
}); });
it('should have one published package', () => { it('should have one published package', () => {
cy.visit(ctx.url); cy.visit(ctx.url);
cy.login(credentials.user, credentials.password); cy.login(credentials.user, credentials.password);
cy.wait('@sign'); cy.wait('@sign');
cy.getByTestId('package-title').should('have.length', 1); // cy.getByTestId('package-title').should('have.length', 1);
}); });
it('should navigate to page detail', () => { it('should navigate to page detail', () => {
@ -25,9 +26,7 @@ describe('publish spec', () => {
cy.wait('@sign'); cy.wait('@sign');
cy.wait('@pkgs'); cy.wait('@pkgs');
cy.wait(300); cy.wait(300);
cy.getByTestId('package-title').click(); cy.getByTestId('package-title').first().click();
cy.wait('@sidebar');
cy.wait('@readme');
}); });
it('should have readme content', () => { it('should have readme content', () => {
@ -35,9 +34,9 @@ describe('publish spec', () => {
cy.login(credentials.user, credentials.password); cy.login(credentials.user, credentials.password);
cy.wait('@sign'); cy.wait('@sign');
cy.wait('@pkgs'); cy.wait('@pkgs');
cy.getByTestId('package-title').click(); cy.getByTestId('package-title').first().click();
cy.wait('@sidebar');
cy.wait('@readme'); cy.wait('@readme');
cy.wait('@sidebar');
cy.get('.markdown-body').should('have.length', 1); cy.get('.markdown-body').should('have.length', 1);
cy.contains('.markdown-body', /test/); cy.contains('.markdown-body', /test/);
}); });
@ -48,9 +47,9 @@ describe('publish spec', () => {
cy.wait('@sign'); cy.wait('@sign');
cy.wait('@pkgs'); cy.wait('@pkgs');
cy.wait(300); cy.wait(300);
cy.getByTestId('package-title').click(); cy.getByTestId('package-title').first().click();
cy.wait('@sidebar');
cy.wait('@readme'); cy.wait('@readme');
cy.wait('@sidebar');
cy.getByTestId('dependencies-tab').click(); cy.getByTestId('dependencies-tab').click();
cy.wait(100); cy.wait(100);
cy.getByTestId('dependencies').should('have.length', 1); cy.getByTestId('dependencies').should('have.length', 1);
@ -67,9 +66,9 @@ describe('publish spec', () => {
cy.wait('@sign'); cy.wait('@sign');
cy.wait('@pkgs'); cy.wait('@pkgs');
cy.wait(300); cy.wait(300);
cy.getByTestId('package-title').click(); cy.getByTestId('package-title').first().click();
cy.wait('@sidebar');
cy.wait('@readme'); cy.wait('@readme');
cy.wait('@sidebar');
cy.getByTestId('versions-tab').click(); cy.getByTestId('versions-tab').click();
cy.getByTestId('tag-latest').children().invoke('text').should('match', /1.0.6/); cy.getByTestId('tag-latest').children().invoke('text').should('match', /1.0.6/);
cy.screenshot(); cy.screenshot();
@ -81,9 +80,9 @@ describe('publish spec', () => {
cy.wait('@sign'); cy.wait('@sign');
cy.wait('@pkgs'); cy.wait('@pkgs');
cy.wait(300); cy.wait(300);
cy.getByTestId('package-title').click(); cy.getByTestId('package-title').first().click();
cy.wait('@sidebar');
cy.wait('@readme'); cy.wait('@readme');
cy.wait('@sidebar');
cy.getByTestId('uplinks-tab').click(); cy.getByTestId('uplinks-tab').click();
cy.getByTestId('no-uplinks').should('be.visible'); cy.getByTestId('no-uplinks').should('be.visible');
cy.screenshot(); cy.screenshot();

@ -25,6 +25,7 @@ describe('sign spec', () => {
cy.wait(100); cy.wait(100);
cy.getByTestId('logOutDialogIcon').click(); cy.getByTestId('logOutDialogIcon').click();
cy.screenshot(); cy.screenshot();
cy.wait(200);
cy.getByTestId('header--button-login').contains('Login'); cy.getByTestId('header--button-login').contains('Login');
cy.screenshot(); cy.screenshot();
}); });

@ -8,7 +8,7 @@
"@verdaccio/config": "workspace:6.0.0-6-next.50", "@verdaccio/config": "workspace:6.0.0-6-next.50",
"@verdaccio/test-helper": "workspace:2.0.0-6-next.6", "@verdaccio/test-helper": "workspace:2.0.0-6-next.6",
"debug": "4.3.4", "debug": "4.3.4",
"cypress": "10.10.0" "cypress": "11.0.1"
}, },
"scripts": { "scripts": {
"cypress:open": "cypress open", "cypress:open": "cypress open",

@ -6,4 +6,22 @@ export const handlers = [
rest.get('http://localhost:9000/-/verdaccio/data/packages', (req, res, ctx) => { rest.get('http://localhost:9000/-/verdaccio/data/packages', (req, res, ctx) => {
return res(ctx.json(packagesPayload)); return res(ctx.json(packagesPayload));
}), }),
rest.post<{ username: string; password: string }, { token: string; username: string }>(
'http://localhost:9000/-/verdaccio/sec/login',
// @ts-ignore
async (req, res, ctx) => {
const body = await req.json();
if (body.username === 'fail') {
return ctx.status(401);
}
return res(
ctx.json({
username: body.username,
token: 'valid token',
})
);
}
),
]; ];

@ -13,30 +13,30 @@
"homepage": "https://verdaccio.org", "homepage": "https://verdaccio.org",
"main": "index.js", "main": "index.js",
"devDependencies": { "devDependencies": {
"@types/react": "17.0.50",
"@types/react-autosuggest": "10.1.5",
"@types/react-dom": "17.0.17",
"@types/react-helmet": "6.1.5",
"@types/redux": "3.6.0",
"@types/react-router-dom": "5.3.3",
"@types/react-virtualized": "9.21.21",
"@emotion/react": "11.10.4",
"@emotion/jest": "11.10.0",
"@emotion/styled": "11.10.4",
"@emotion/css": "11.10.0",
"@emotion/babel-plugin": "11.10.2", "@emotion/babel-plugin": "11.10.2",
"@emotion/css": "11.10.0",
"@emotion/jest": "11.10.0",
"@emotion/react": "11.10.4",
"@emotion/styled": "11.10.4",
"@mui/icons-material": "5.10.9", "@mui/icons-material": "5.10.9",
"@mui/material": "5.10.9", "@mui/material": "5.10.9",
"@mui/styles": "5.10.9", "@mui/styles": "5.10.9",
"@rematch/core": "2.2.0", "@rematch/core": "2.2.0",
"@rematch/loading": "2.1.2", "@rematch/loading": "2.1.2",
"@rematch/persist": "2.1.2",
"@testing-library/dom": "8.19.0", "@testing-library/dom": "8.19.0",
"@testing-library/jest-dom": "5.16.5", "@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "12.1.5", "@testing-library/react": "13.4.0",
"@types/react": "18.0.25",
"@types/react-dom": "18.0.8",
"@types/react-router-dom": "5.3.3",
"@types/react-virtualized": "9.21.21",
"@types/redux": "3.6.0",
"@verdaccio/node-api": "workspace:6.0.0-6-next.50", "@verdaccio/node-api": "workspace:6.0.0-6-next.50",
"@verdaccio/types": "workspace:*", "@verdaccio/types": "workspace:*",
"babel-loader": "8.2.5", "babel-loader": "8.2.5",
"babel-plugin-dynamic-import-node": "2.3.3", "babel-plugin-dynamic-import-node": "2.3.3",
"country-flag-icons": "1.5.5",
"css-loader": "6.7.1", "css-loader": "6.7.1",
"dayjs": "1.11.5", "dayjs": "1.11.5",
"friendly-errors-webpack-plugin": "1.7.0", "friendly-errors-webpack-plugin": "1.7.0",
@ -46,33 +46,33 @@
"html-webpack-plugin": "5.5.0", "html-webpack-plugin": "5.5.0",
"i18next": "20.6.1", "i18next": "20.6.1",
"in-publish": "2.0.1", "in-publish": "2.0.1",
"country-flag-icons": "1.5.5",
"js-base64": "3.7.2", "js-base64": "3.7.2",
"js-yaml": "3.14.1", "js-yaml": "3.14.1",
"localstorage-memory": "1.0.3", "localstorage-memory": "1.0.3",
"lodash": "4.17.21", "lodash": "4.17.21",
"mini-css-extract-plugin": "2.6.1", "mini-css-extract-plugin": "2.6.1",
"msw": "0.47.4",
"mutationobserver-shim": "0.3.7", "mutationobserver-shim": "0.3.7",
"node-mocks-http": "1.11.0", "node-mocks-http": "1.11.0",
"normalize.css": "8.0.1", "normalize.css": "8.0.1",
"react-markdown": "8.0.3",
"react-json-view": "1.21.3",
"remark-gfm": "3.0.1",
"optimize-css-assets-webpack-plugin": "6.0.1", "optimize-css-assets-webpack-plugin": "6.0.1",
"ora": "5.4.1", "ora": "5.4.1",
"react": "17.0.2", "raw-loader": "4.0.2",
"react-dom": "17.0.2", "react": "18.2.0",
"react-hook-form": "7.37.0", "react-dom": "18.2.0",
"react-hook-form": "7.39.2",
"react-hot-loader": "4.13.0", "react-hot-loader": "4.13.0",
"react-i18next": "11.18.6", "react-i18next": "12.0.0",
"react-json-view": "1.21.3",
"react-markdown": "8.0.3",
"react-redux": "7.2.9",
"react-router": "5.3.4", "react-router": "5.3.4",
"react-router-dom": "5.3.4", "react-router-dom": "5.3.4",
"react-virtualized": "9.22.3", "react-virtualized": "9.22.3",
"react-redux": "7.2.9",
"redux": "4.2.0", "redux": "4.2.0",
"redux-persist": "6.0.0",
"remark-gfm": "3.0.1",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"raw-loader": "4.0.2",
"msw": "0.47.4",
"style-loader": "3.3.1", "style-loader": "3.3.1",
"stylelint": "14.14.0", "stylelint": "14.14.0",
"stylelint-config-recommended": "7.0.0", "stylelint-config-recommended": "7.0.0",
@ -86,7 +86,7 @@
"webpack": "5.74.0", "webpack": "5.74.0",
"webpack-bundle-analyzer": "4.6.1", "webpack-bundle-analyzer": "4.6.1",
"webpack-bundle-size-analyzer": "3.1.0", "webpack-bundle-size-analyzer": "3.1.0",
"webpack-cli": "^4.7.2", "webpack-cli": "^4.10.0",
"webpack-dev-server": "3.11.3", "webpack-dev-server": "3.11.3",
"webpack-manifest-plugin": "4.1.1", "webpack-manifest-plugin": "4.1.1",
"webpack-merge": "5.8.0", "webpack-merge": "5.8.0",

@ -1,14 +1,6 @@
import React from 'react'; import React from 'react';
import { import { renderWithStore, screen } from 'verdaccio-ui/utils/test-react-testing-library';
act,
fireEvent,
renderWithStore,
screen,
waitFor,
} 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 { store } from '../store';
import App from './App'; import App from './App';
@ -41,84 +33,6 @@ jest.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockReturnValue(600);
/* eslint-disable react/jsx-no-bind*/ /* eslint-disable react/jsx-no-bind*/
describe('<App />', () => { describe('<App />', () => {
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),
});
// 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 Button's logout element component appearance and return the element
const buttonLogoutElement = await waitFor(() => queryByTestId('logOutDialogIcon'));
expect(buttonLogoutElement).toBeTruthy();
if (buttonLogoutElement) {
fireEvent.click(buttonLogoutElement);
expect(queryByTestId('greetings-label')).toBeFalsy();
}
}
}, 10000);
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();
}
}
}, 10000);
});
describe('list packages', () => {
test('should display the Header component', async () => {
renderWithStore(<App />, store);
await waitFor(() => {
expect(screen.queryByTestId('loading')).toBeTruthy();
});
// wait for the Header component appearance and return the element
const headerElement = await waitFor(() => screen.queryByTestId('header'));
expect(headerElement).toBeTruthy();
});
test('should display package lists', async () => {
act(() => {
renderWithStore(<App />, store);
});
await waitFor(() => {
expect(screen.getByTestId('package-item-list')).toBeInTheDocument();
});
expect(store.getState().packages.response).toHaveLength(1);
}, 10000);
});
describe('footer', () => { describe('footer', () => {
test('should display the Header component', () => { test('should display the Header component', () => {
renderWithStore(<App />, store); renderWithStore(<App />, store);

@ -1,7 +1,7 @@
/* eslint-disable react/jsx-max-depth */ /* eslint-disable react/jsx-max-depth */
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import React, { Suspense, useEffect } from 'react'; import React, { StrictMode, Suspense, useEffect } from 'react';
import { Router } from 'react-router-dom'; import { Router } from 'react-router-dom';
import Loading from 'verdaccio-ui/components/Loading'; import Loading from 'verdaccio-ui/components/Loading';
import StyleBaseline from 'verdaccio-ui/design-tokens/StyleBaseline'; import StyleBaseline from 'verdaccio-ui/design-tokens/StyleBaseline';
@ -34,20 +34,22 @@ const App: React.FC = () => {
loadDayJSLocale(); loadDayJSLocale();
}, []); }, []);
return ( return (
<Suspense fallback={<Loading />}> <StrictMode>
<StyleBaseline /> <Suspense fallback={<Loading />}>
<StyledBox display="flex" flexDirection="column" height="100%"> <StyleBaseline />
<> <StyledBox display="flex" flexDirection="column" height="100%">
<Router history={history}> <>
<Header /> <Router history={history}>
<StyledBoxContent flexGrow={1}> <Header />
<AppRoute /> <StyledBoxContent flexGrow={1}>
</StyledBoxContent> <AppRoute />
</Router> </StyledBoxContent>
{configOptions.showFooter && <Footer />} </Router>
</> {configOptions.showFooter && <Footer />}
</StyledBox> </>
</Suspense> </StyledBox>
</Suspense>
</StrictMode>
); );
}; };

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
import { import {
act,
cleanup, cleanup,
fireEvent, fireEvent,
renderWithStore, renderWithStore,
@ -15,9 +16,15 @@ import Header from './Header';
/* eslint-disable react/jsx-no-bind*/ /* eslint-disable react/jsx-no-bind*/
describe('<Header /> component with logged in state', () => { describe('<Header /> component with logged in state', () => {
afterEach(cleanup); beforeEach(() => {
store.dispatch.login.logOutUser();
});
test('should load the component in logged out state', () => { afterEach(() => {
cleanup();
});
test('should load the component n logged out state', () => {
renderWithStore( renderWithStore(
<Router> <Router>
<Header /> <Header />
@ -53,15 +60,15 @@ describe('<Header /> component with logged in state', () => {
store store
); );
store.dispatch.login.logOutUser();
const loginBtn = screen.getByTestId('header--button-login'); const loginBtn = screen.getByTestId('header--button-login');
fireEvent.click(loginBtn); fireEvent.click(loginBtn);
const loginDialog = await waitFor(() => screen.getByTestId('login--dialog')); const loginDialog = await waitFor(() => screen.getByTestId('login--dialog'));
expect(loginDialog).toBeTruthy(); expect(loginDialog).toBeTruthy();
}); });
test('should logout the user', async () => { test('should login and logout the user', async () => {
const { getByText, getByTestId } = renderWithStore( const { getByText, getByTestId } = renderWithStore(
<Router> <Router>
<Header /> <Header />
@ -69,14 +76,27 @@ describe('<Header /> component with logged in state', () => {
store store
); );
store.dispatch.login.logInUser({ username: 'store', token: '12345' }); fireEvent.click(screen.getByText('Login'));
const userNameInput = screen.getByPlaceholderText('Your username');
fireEvent.focus(userNameInput);
fireEvent.change(userNameInput, { target: { value: 'xyz' } });
const passwordInput = screen.getByPlaceholderText('Your strong password');
fireEvent.focus(passwordInput);
await act(async () => {
fireEvent.change(passwordInput, { target: { value: '1234' } });
});
const signInButton = screen.getByTestId('login-dialog-form-login-button');
await act(async () => {
fireEvent.click(signInButton);
});
await waitFor(() => getByTestId('logInDialogIcon'));
const headerMenuAccountCircle = getByTestId('logInDialogIcon'); const headerMenuAccountCircle = getByTestId('logInDialogIcon');
fireEvent.click(headerMenuAccountCircle); fireEvent.click(headerMenuAccountCircle);
// wait for button Logout's appearance and return the element // // wait for button Logout's appearance and return the element
const logoutBtn = await waitFor(() => getByText('Logout')); const logoutBtn = await waitFor(() => getByText('Logout'));
fireEvent.click(logoutBtn); fireEvent.click(logoutBtn);
await waitFor(() => getByText('Login'));
expect(getByText('Login')).toBeTruthy(); expect(getByText('Login')).toBeTruthy();
}); });

@ -25,6 +25,7 @@ const Header: React.FC = () => {
const dispatch = useDispatch<Dispatch>(); const dispatch = useDispatch<Dispatch>();
const handleLogout = () => { const handleLogout = () => {
dispatch.login.logOutUser(); dispatch.login.logOutUser();
setShowLoginModal(false);
}; };
return ( return (
<> <>

@ -69,6 +69,7 @@ const LoginDialogForm = memo(({ onSubmit, error }: Props) => {
required: { value: true, message: t('form-validation.required-field') }, required: { value: true, message: t('form-validation.required-field') },
minLength: { value: 2, message: t('form-validation.required-min-length', { length: 2 }) }, minLength: { value: 2, message: t('form-validation.required-min-length', { length: 2 }) },
})} })}
data-testid="password"
label={t('form.password')} label={t('form.password')}
margin="normal" margin="normal"
name="password" name="password"

@ -3,6 +3,7 @@ import BugReportIcon from '@mui/icons-material/BugReport';
import DownloadIcon from '@mui/icons-material/CloudDownload'; import DownloadIcon from '@mui/icons-material/CloudDownload';
import HomeIcon from '@mui/icons-material/Home'; import HomeIcon from '@mui/icons-material/Home';
import RawOnIcon from '@mui/icons-material/RawOn'; import RawOnIcon from '@mui/icons-material/RawOn';
import FabMUI from '@mui/material/Fab';
import Tooltip from '@mui/material/Tooltip'; import Tooltip from '@mui/material/Tooltip';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -10,10 +11,9 @@ import { useDispatch } from 'react-redux';
import { Theme } from 'verdaccio-ui/design-tokens/theme'; import { Theme } from 'verdaccio-ui/design-tokens/theme';
import { Dispatch } from '../../store/store'; import { Dispatch } from '../../store/store';
import FloatingActionButton from '../FloatingActionButton';
import Link from '../Link'; import Link from '../Link';
export const Fab = styled(FloatingActionButton)<{ theme?: Theme }>(({ theme }) => ({ export const Fab = styled(FabMUI)<{ theme?: Theme }>(({ theme }) => ({
backgroundColor: backgroundColor:
theme?.palette.mode === 'light' ? theme?.palette.primary.main : theme?.palette.cyanBlue, theme?.palette.mode === 'light' ? theme?.palette.primary.main : theme?.palette.cyanBlue,
color: theme?.palette.white, color: theme?.palette.white,

@ -168,7 +168,6 @@ exports[`<ActionBar /> component should render the component in default state 1`
> >
<button <button
class="MuiButtonBase-root MuiFab-root MuiFab-circular MuiFab-sizeSmall MuiFab-default MuiFab-root MuiFab-circular MuiFab-sizeSmall MuiFab-default emotion-2 emotion-3" class="MuiButtonBase-root MuiFab-root MuiFab-circular MuiFab-sizeSmall MuiFab-default MuiFab-root MuiFab-circular MuiFab-sizeSmall MuiFab-default emotion-2 emotion-3"
data-testid="fab"
tabindex="0" tabindex="0"
type="button" type="button"
> >
@ -202,7 +201,6 @@ exports[`<ActionBar /> component should render the component in default state 1`
> >
<button <button
class="MuiButtonBase-root MuiFab-root MuiFab-circular MuiFab-sizeSmall MuiFab-default MuiFab-root MuiFab-circular MuiFab-sizeSmall MuiFab-default emotion-2 emotion-3" class="MuiButtonBase-root MuiFab-root MuiFab-circular MuiFab-sizeSmall MuiFab-default MuiFab-root MuiFab-circular MuiFab-sizeSmall MuiFab-default emotion-2 emotion-3"
data-testid="fab"
tabindex="0" tabindex="0"
type="button" type="button"
> >
@ -227,7 +225,7 @@ exports[`<ActionBar /> component should render the component in default state 1`
aria-label="Download tarball" aria-label="Download tarball"
class="MuiButtonBase-root MuiFab-root MuiFab-circular MuiFab-sizeSmall MuiFab-default MuiFab-root MuiFab-circular MuiFab-sizeSmall MuiFab-default emotion-2 emotion-3" class="MuiButtonBase-root MuiFab-root MuiFab-circular MuiFab-sizeSmall MuiFab-default MuiFab-root MuiFab-circular MuiFab-sizeSmall MuiFab-default emotion-2 emotion-3"
data-mui-internal-clone-element="true" data-mui-internal-clone-element="true"
data-testid="fab" data-testid="download-tarball-btn"
tabindex="0" tabindex="0"
type="button" type="button"
> >

@ -1,8 +0,0 @@
import Fab from '@mui/material/Fab';
import React from 'react';
const FloatingActionButton = (props) => {
return <Fab {...props} data-testid="fab" />;
};
export default FloatingActionButton;

@ -212,7 +212,7 @@ label[data-shrink=false]+.MuiInputBase-formControl .emotion-2:focus::-ms-input-p
<input <input
aria-invalid="false" aria-invalid="false"
class="MuiInputBase-input MuiOutlinedInput-input emotion-2" class="MuiInputBase-input MuiOutlinedInput-input emotion-2"
id="mui-1" id=":r0:"
name="test" name="test"
type="text" type="text"
value="test" value="test"

@ -22,7 +22,7 @@ function getDarkModeDefault(darkModeConfig) {
} }
} }
const ThemeProviderWrapper: React.FC = ({ children }) => { const ThemeProviderWrapper: React.FC<{ children: any }> = ({ children }) => {
const currentLanguage = i18next.languages?.[0]; const currentLanguage = i18next.languages?.[0];
const { configOptions } = useConfig(); const { configOptions } = useConfig();
const isDarkModeDefault = getDarkModeDefault(configOptions.darkMode); const isDarkModeDefault = getDarkModeDefault(configOptions.darkMode);

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import { createRoot } from 'react-dom/client';
import { AppContainer } from 'react-hot-loader'; import { hot } from 'react-hot-loader/root';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import AppConfigurationContext from 'verdaccio-ui/providers/config'; import AppConfigurationContext from 'verdaccio-ui/providers/config';
@ -9,29 +9,23 @@ import StyleBaseline from './design-tokens/StyleBaseline';
import ThemeProvider from './design-tokens/ThemeProvider'; import ThemeProvider from './design-tokens/ThemeProvider';
import { store } from './store'; import { store } from './store';
const rootNode = document.getElementById('root'); const container = document.getElementById('root');
const renderApp = (Component: React.ElementType): void => { const root = createRoot(container as HTMLElement);
ReactDOM.render(
<Provider store={store}>
<AppContainer>
<AppConfigurationContext>
<ThemeProvider>
<StyleBaseline />
<Component />
</ThemeProvider>
</AppConfigurationContext>
</AppContainer>
</Provider>,
rootNode
);
};
renderApp(App); const AppContainer = () => (
<Provider store={store}>
<AppConfigurationContext>
<ThemeProvider>
<StyleBaseline />
<App />
</ThemeProvider>
</AppConfigurationContext>
</Provider>
);
root.render(<AppContainer />);
// @ts-expect-error // @ts-expect-error
if (module.hot) { if (module.hot) {
// @ts-expect-error hot(AppContainer);
module.hot.accept('./App', () => {
renderApp(App);
});
} }

@ -1,6 +1,6 @@
import _ from 'lodash'; import _ from 'lodash';
import React from 'react'; import React from 'react';
import { renderWithStore, screen } from 'verdaccio-ui/utils/test-react-testing-library'; import { renderWithStore, screen, waitFor } from 'verdaccio-ui/utils/test-react-testing-library';
import { store } from '../../../store'; import { store } from '../../../store';
import { DetailContext } from '../context'; import { DetailContext } from '../context';
@ -15,6 +15,13 @@ const ComponentToBeRendered: React.FC<{ contextValue: DetailContextProps }> = ({
</DetailContext.Provider> </DetailContext.Provider>
); );
// https://stackoverflow.com/a/54010619/308341
jest.mock('react', () => {
const React = jest.requireActual('react');
React.Suspense = ({ children }) => children;
return React;
});
const detailContextValue: DetailContextProps = { const detailContextValue: DetailContextProps = {
packageName: 'foo', packageName: 'foo',
readMe: 'test', readMe: 'test',
@ -40,8 +47,8 @@ const detailContextValue: DetailContextProps = {
}; };
describe('DetailSidebar', () => { describe('DetailSidebar', () => {
test('should render commonjs module icon', () => { test('should render commonjs module icon', async () => {
renderWithStore( const { getByAltText } = renderWithStore(
<ComponentToBeRendered <ComponentToBeRendered
contextValue={_.merge(detailContextValue, { contextValue={_.merge(detailContextValue, {
packageMeta: { packageMeta: {
@ -53,7 +60,8 @@ describe('DetailSidebar', () => {
/>, />,
store store
); );
expect(screen.getByAltText('commonjs')).toBeInTheDocument();
await waitFor(() => expect(getByAltText('commonjs')).toBeInTheDocument());
}); });
test('should render ts module icon', () => { test('should render ts module icon', () => {

@ -107,7 +107,7 @@ describe('test Developers', () => {
// item2.simulate('click'); // item2.simulate('click');
expect(wrapper.getByText('Contributors')).toBeInTheDocument(); expect(wrapper.getByText('Contributors')).toBeInTheDocument();
fireEvent.click(wrapper.getByTestId('fab')); fireEvent.click(wrapper.getByRole('button'));
expect(wrapper.getByLabelText(packageMeta.latest.contributors[0].name)).toBeInTheDocument(); expect(wrapper.getByLabelText(packageMeta.latest.contributors[0].name)).toBeInTheDocument();
expect(wrapper.getByLabelText(packageMeta.latest.contributors[1].name)).toBeInTheDocument(); expect(wrapper.getByLabelText(packageMeta.latest.contributors[1].name)).toBeInTheDocument();

@ -2,10 +2,10 @@ import styled from '@emotion/styled';
import Add from '@mui/icons-material/Add'; import Add from '@mui/icons-material/Add';
import Avatar from '@mui/material/Avatar'; import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import FabMUI from '@mui/material/Fab';
import Tooltip from '@mui/material/Tooltip'; import Tooltip from '@mui/material/Tooltip';
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import FloatingActionButton from 'verdaccio-ui/components/FloatingActionButton';
import { Theme } from 'verdaccio-ui/design-tokens/theme'; import { Theme } from 'verdaccio-ui/design-tokens/theme';
import { DetailContext } from '../..'; import { DetailContext } from '../..';
@ -17,7 +17,7 @@ export enum DeveloperType {
MAINTAINERS = 'maintainers', MAINTAINERS = 'maintainers',
} }
export const Fab = styled(FloatingActionButton)<{ theme?: Theme }>((props) => ({ export const Fab = styled(FabMUI)<{ theme?: Theme }>((props) => ({
backgroundColor: props.theme?.palette.primary.main, backgroundColor: props.theme?.palette.primary.main,
color: props.theme?.palette.white, color: props.theme?.palette.white,
})); }));
@ -70,11 +70,13 @@ const Developers: React.FC<Props> = ({ type, visibleMax = VISIBLE_MAX }) => {
<> <>
<DevelopersTitle type={type} /> <DevelopersTitle type={type} />
<StyledBox display="flex" flexWrap="wrap" margin="10px 0 10px 0"> <StyledBox display="flex" flexWrap="wrap" margin="10px 0 10px 0">
{visibleDevelopers.map((visibleDeveloper) => ( {visibleDevelopers.map((visibleDeveloper) => {
<Tooltip key={visibleDeveloper.email} title={visibleDeveloper.name}> return (
<Avatar alt={visibleDeveloper.name} src={visibleDeveloper.avatar} /> <Tooltip key={visibleDeveloper.email} title={visibleDeveloper.name}>
</Tooltip> <Avatar alt={visibleDeveloper.name} src={visibleDeveloper.avatar} />
))} </Tooltip>
);
})}
{visibleDevelopersMax < developers.length && ( {visibleDevelopersMax < developers.length && (
<Fab onClick={handleSetVisibleDevelopersMax} size="small"> <Fab onClick={handleSetVisibleDevelopersMax} size="small">
<Add /> <Add />

@ -1,5 +1,5 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import FloatingActionButton from 'verdaccio-ui/components/FloatingActionButton'; import FabMUI from '@mui/material/Fab';
import Text from 'verdaccio-ui/components/Text'; import Text from 'verdaccio-ui/components/Text';
import { Theme } from 'verdaccio-ui/design-tokens/theme'; import { Theme } from 'verdaccio-ui/design-tokens/theme';
@ -24,7 +24,7 @@ export const StyledText = styled(Text)<{ theme?: Theme }>((props) => ({
textTransform: 'capitalize', textTransform: 'capitalize',
})); }));
export const Fab = styled(FloatingActionButton)<{ theme?: Theme }>((props) => ({ export const Fab = styled(FabMUI)<{ theme?: Theme }>((props) => ({
backgroundColor: props.theme?.palette.primary.main, backgroundColor: props.theme?.palette.primary.main,
color: props.theme?.palette.white, color: props.theme?.palette.white,
})); }));

@ -1,7 +1,7 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import Chip from '@mui/material/Chip'; import Chip from '@mui/material/Chip';
import FabMUI from '@mui/material/Fab';
import ListItem from '@mui/material/ListItem'; import ListItem from '@mui/material/ListItem';
import FloatingActionButton from 'verdaccio-ui/components/FloatingActionButton';
import Text from 'verdaccio-ui/components/Text'; import Text from 'verdaccio-ui/components/Text';
import { Theme } from 'verdaccio-ui/design-tokens/theme'; import { Theme } from 'verdaccio-ui/design-tokens/theme';
@ -22,7 +22,7 @@ export const DistChips = styled(Chip)({
marginTop: 5, marginTop: 5,
}); });
export const DownloadButton = styled(FloatingActionButton)<{ theme?: Theme }>((props) => ({ export const DownloadButton = styled(FabMUI)<{ theme?: Theme }>((props) => ({
backgroundColor: props.theme && props.theme.palette.primary.main, backgroundColor: props.theme && props.theme.palette.primary.main,
color: props.theme && props.theme.palette.white, color: props.theme && props.theme.palette.white,
})); }));

@ -6,7 +6,6 @@ import List from '@mui/material/List';
import ListItemText from '@mui/material/ListItemText'; import ListItemText from '@mui/material/ListItemText';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import Label from 'verdaccio-ui/components/Label'; import Label from 'verdaccio-ui/components/Label';
import { default as MuiText } from 'verdaccio-ui/components/Text';
import { Theme } from 'verdaccio-ui/design-tokens/theme'; import { Theme } from 'verdaccio-ui/design-tokens/theme';
export const OverviewItem = styled('span')<{ theme?: Theme }>(({ theme }) => ({ export const OverviewItem = styled('span')<{ theme?: Theme }>(({ theme }) => ({
@ -104,7 +103,7 @@ export const PackageListItemText = styled(ListItemText)({
paddingRight: 0, paddingRight: 0,
}); });
export const Description = styled(MuiText)<{ theme?: Theme }>(({ theme }) => ({ export const Description = styled('span')<{ theme?: Theme }>(({ theme }) => ({
color: theme?.palette.mode === 'light' ? theme?.palette.greyDark2 : theme?.palette.white, color: theme?.palette.mode === 'light' ? theme?.palette.greyDark2 : theme?.palette.white,
fontSize: '14px', fontSize: '14px',
paddingRight: 0, paddingRight: 0,

@ -1,16 +1,16 @@
import { createModel } from '@rematch/core'; import { createModel } from '@rematch/core';
import { Package } from '@verdaccio/types'; import { Manifest } from '@verdaccio/types';
import type { RootModel } from '.'; import type { RootModel } from '.';
import API from '../../providers/API/api'; import API from '../../providers/API/api';
export const packages = createModel<RootModel>()({ export const packages = createModel<RootModel>()({
state: { state: {
response: [] as Package[], response: [] as Manifest[],
}, },
reducers: { reducers: {
savePackages(state, response: Package[]) { savePackages(state, response: Manifest[]) {
return { return {
...state, ...state,
response, response,
@ -21,7 +21,10 @@ export const packages = createModel<RootModel>()({
async getPackages(_payload, state) { async getPackages(_payload, state) {
const basePath = state.configuration.config.base; const basePath = state.configuration.config.base;
try { try {
const payload: Package[] = await API.request(`${basePath}-/verdaccio/data/packages`, 'GET'); const payload: Manifest[] = await API.request(
`${basePath}-/verdaccio/data/packages`,
'GET'
);
dispatch.packages.savePackages(payload); dispatch.packages.savePackages(payload);
} catch (error: any) { } catch (error: any) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console

661
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff