@verdaccio/ui-components 4.0.0-next-8.5 → 4.0.0-next-8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @verdaccio/ui-components
2
2
 
3
+ ## 4.0.0-next-8.6
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [b6e9f46]
8
+ - @verdaccio/ui-i18n@8.0.0-next-8.11
9
+
3
10
  ## 4.0.0-next-8.5
4
11
 
5
12
  ### Minor Changes
@@ -1,3 +1,6 @@
1
+ var _excluded = ["key"];
2
+ function _objectWithoutProperties(e, t) { if (null == e) return {}; var o, r, i = _objectWithoutPropertiesLoose(e, t); if (Object.getOwnPropertySymbols) { var n = Object.getOwnPropertySymbols(e); for (r = 0; r < n.length; r++) o = n[r], -1 === t.indexOf(o) && {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]); } return i; }
3
+ function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
1
4
  function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
2
5
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
3
6
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
@@ -121,12 +124,18 @@ var Search = _ref => {
121
124
  }
122
125
  };
123
126
  var renderOption = (props, option) => {
127
+ var {
128
+ key
129
+ } = props,
130
+ otherProps = _objectWithoutProperties(props, _excluded);
124
131
  if (searchRemote) {
125
132
  var item = option.package;
126
133
  var isPrivate = option === null || option === void 0 ? void 0 : option.verdaccioPrivate;
127
134
  var isCached = option === null || option === void 0 ? void 0 : option.verdaccioPkgCached;
128
135
  var isRemote = !isCached && !isPrivate;
129
- return /*#__PURE__*/React.createElement(SearchItem, _extends({}, props, {
136
+ return /*#__PURE__*/React.createElement(SearchItem, _extends({
137
+ key: key
138
+ }, otherProps, {
130
139
  description: item === null || item === void 0 ? void 0 : item.description,
131
140
  isCached: isCached,
132
141
  isPrivate: isPrivate,
@@ -135,7 +144,9 @@ var Search = _ref => {
135
144
  version: item === null || item === void 0 ? void 0 : item.version
136
145
  }));
137
146
  } else {
138
- return /*#__PURE__*/React.createElement(SearchItem, _extends({}, props, {
147
+ return /*#__PURE__*/React.createElement(SearchItem, _extends({
148
+ key: key
149
+ }, otherProps, {
139
150
  description: option === null || option === void 0 ? void 0 : option.description,
140
151
  name: option === null || option === void 0 ? void 0 : option.name,
141
152
  version: option === null || option === void 0 ? void 0 : option.version
@@ -1,8 +1,9 @@
1
- declare const _default: {
2
- title: string;
3
- };
4
- export default _default;
5
- export declare const ApplicationStoryBook: any;
6
- export declare const ApplicationJquery: any;
7
- export declare const ApplicationForbidden: any;
8
- export declare const ApplicationNotFound: any;
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import AppRoute from './AppRoute';
3
+ declare const meta: Meta<typeof AppRoute>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof AppRoute>;
6
+ export declare const ApplicationStoryBook: Story;
7
+ export declare const ApplicationJquery: Story;
8
+ export declare const ApplicationForbidden: Story;
9
+ export declare const ApplicationNotFound: Story;
@@ -1,4 +1,6 @@
1
1
  import React from 'react';
2
2
  import { RouteComponentProps } from 'react-router';
3
- declare const _default: React.ComponentClass<Pick<RouteComponentProps<{}, import("react-router").StaticContext, H.LocationState>, never>, any> & import("react-router").WithRouterStatics<React.FC<RouteComponentProps<{}, import("react-router").StaticContext, H.LocationState>>>;
3
+ declare const _default: React.ComponentClass<Pick<RouteComponentProps<any, import("react-router").StaticContext, H.LocationState>, never> | (Pick<RouteComponentProps<any, import("react-router").StaticContext, H.LocationState>, never> & {
4
+ wrappedComponentRef?: React.Ref<any> | undefined;
5
+ }), any> & import("react-router").WithRouterStatics<any>;
4
6
  export default _default;
@@ -1,8 +1,6 @@
1
- import React from 'react';
2
- declare const _default: {
3
- title: string;
4
- };
5
- export default _default;
6
- export declare const SearchByQuery: {
7
- render: (args: any) => React.JSX.Element;
8
- };
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import Search from './Search';
3
+ type Story = StoryObj<typeof Search>;
4
+ declare const meta: Meta<typeof Search>;
5
+ export default meta;
6
+ export declare const SearchByQuery: Story;
@@ -1,7 +1,8 @@
1
- declare const _default: {
2
- title: string;
3
- };
4
- export default _default;
5
- export declare const DetailStorybook: any;
6
- export declare const DetailJquery: any;
7
- export declare const DetailForbidden: any;
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import Detail from './Detail';
3
+ declare const meta: Meta<typeof Detail>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof Detail>;
6
+ export declare const DetailStorybook: Story;
7
+ export declare const DetailJquery: Story;
8
+ export declare const DetailForbidden: Story;
@@ -1,5 +1,6 @@
1
- declare const _default: {
2
- title: string;
3
- };
4
- export default _default;
5
- export declare const HeaderAll: any;
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import Header from './Header';
3
+ type Story = StoryObj<typeof Header>;
4
+ declare const meta: Meta<typeof Header>;
5
+ export default meta;
6
+ export declare const HeaderAll: Story;
@@ -1,5 +1,6 @@
1
- declare const _default: {
2
- title: string;
3
- };
4
- export default _default;
5
- export declare const HomeDefault: any;
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import Home from './Home';
3
+ type Story = StoryObj<typeof Home>;
4
+ declare const meta: Meta<typeof Home>;
5
+ export default meta;
6
+ export declare const HomeDefault: Story;
@@ -1,7 +1,7 @@
1
- declare const _default: {
2
- title: string;
3
- };
4
- export default _default;
5
- export declare const SidebarLatestPackage: any;
6
- export declare const SidebarPackageVersion: any;
7
- export declare const SidebarNoFounding: any;
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import DetailSidebar from './Sidebar';
3
+ declare const meta: Meta<typeof DetailSidebar>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof DetailSidebar>;
6
+ export declare const SidebarLatestPackage: Story;
7
+ export declare const SidebarPackageVersion: Story;
@@ -41,14 +41,24 @@ export function downloadFile(fileStream, fileName) {
41
41
  fileLink.href = objectURL;
42
42
  fileLink.download = fileName;
43
43
 
44
- // Without appending to an HTML Element, download dialog does not show up on Firefox
45
- // https://github.com/verdaccio/ui/issues/119
46
- document.documentElement.appendChild(fileLink);
47
- fileLink.click();
48
- // firefox requires remove the object url
49
- setTimeout(() => {
50
- URL.revokeObjectURL(objectURL);
51
- document.documentElement.removeChild(fileLink);
52
- }, 150);
44
+ // Skip actual download in test environment to avoid JSDOM navigation error
45
+ // Check if we're in a test environment (JSDOM)
46
+ var isTestEnvironment = typeof window !== 'undefined' && window.navigator && window.navigator.userAgent && window.navigator.userAgent.includes('Node.js') || window.navigator.userAgent.includes('jsdom');
47
+ if (!isTestEnvironment) {
48
+ // Without appending to an HTML Element, download dialog does not show up on Firefox
49
+ // https://github.com/verdaccio/ui/issues/119
50
+ document.documentElement.appendChild(fileLink);
51
+ fileLink.click();
52
+ // firefox requires remove the object url
53
+ setTimeout(() => {
54
+ URL.revokeObjectURL(objectURL);
55
+ document.documentElement.removeChild(fileLink);
56
+ }, 150);
57
+ } else {
58
+ // In test environment, just clean up without triggering navigation
59
+ setTimeout(() => {
60
+ URL.revokeObjectURL(objectURL);
61
+ }, 150);
62
+ }
53
63
  }
54
64
  //# sourceMappingURL=url.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verdaccio/ui-components",
3
- "version": "4.0.0-next-8.5",
3
+ "version": "4.0.0-next-8.6",
4
4
  "description": "theme ui component",
5
5
  "author": "Juan Picado <juanpicado19@gmail.com>",
6
6
  "license": "MIT",
@@ -20,7 +20,7 @@
20
20
  "@rematch/core": "2.2.0",
21
21
  "@rematch/loading": "2.1.2",
22
22
  "@rematch/persist": "2.1.2",
23
- "country-flag-icons": "1.5.14",
23
+ "country-flag-icons": "1.5.16",
24
24
  "dayjs": "1.11.13",
25
25
  "dompurify": "3.2.3",
26
26
  "highlight.js": "11.11.1",
@@ -34,7 +34,7 @@
34
34
  "normalize.css": "8.0.1",
35
35
  "react": "18.3.1",
36
36
  "react-dom": "18.3.1",
37
- "react-hook-form": "7.52.1",
37
+ "react-hook-form": "7.54.2",
38
38
  "react-i18next": "15.0.0",
39
39
  "react-markdown": "9.0.3",
40
40
  "react-redux": "8.1.3",
@@ -42,19 +42,21 @@
42
42
  "react-router-dom": "5.3.4",
43
43
  "react-virtualized": "9.22.6",
44
44
  "redux": "4.2.1",
45
- "semver": "7.6.3",
46
- "validator": "13.12.0"
45
+ "semver": "7.7.1",
46
+ "validator": "13.12.0",
47
+ "@verdaccio/ui-i18n": "8.0.0-next-8.11"
47
48
  },
48
49
  "devDependencies": {
49
- "@babel/core": "7.26.7",
50
+ "@babel/core": "7.26.9",
50
51
  "@emotion/babel-plugin": "11.12.0",
51
- "@storybook/addon-actions": "^8.4.7",
52
- "@storybook/addon-essentials": "^8.4.7",
53
- "@storybook/addon-interactions": "^8.4.7",
54
- "@storybook/addon-links": "^8.4.7",
52
+ "@storybook/addon-actions": "^8.6.3",
53
+ "@storybook/addon-essentials": "^8.6.3",
54
+ "@storybook/addon-mdx-gfm": "^8.6.3",
55
+ "@storybook/addon-interactions": "^8.6.3",
56
+ "@storybook/addon-links": "^8.6.3",
55
57
  "@storybook/addon-webpack5-compiler-babel": "3.0.5",
56
- "@storybook/react": "^8.4.7",
57
- "@storybook/react-webpack5": "8.4.7",
58
+ "@storybook/react": "^8.6.3",
59
+ "@storybook/react-webpack5": "8.6.3",
58
60
  "@testing-library/dom": "10.4.0",
59
61
  "@testing-library/jest-dom": "6.6.3",
60
62
  "@testing-library/react": "16.1.0",
@@ -69,7 +71,7 @@
69
71
  "msw": "2.7.0",
70
72
  "msw-storybook-addon": "^2.0.4",
71
73
  "mutationobserver-shim": "0.3.7",
72
- "storybook": "8.4.7",
74
+ "storybook": "8.6.3",
73
75
  "typescript": "4.9.5",
74
76
  "vitest": "3.0.4",
75
77
  "whatwg-fetch": "3.6.20"
@@ -88,7 +90,7 @@
88
90
  "watch": "babel src/ --out-dir build/ --watch --copy-files --no-copy-ignored --extensions \".ts,.tsx\" --source-maps --ignore \"src/**/*.stories.tsx\" --ignore \"src/**/*.test.tsx\" --ignore \"src/**/*.test.ts\"",
89
91
  "build": "pnpm run build:js && pnpm run build:types",
90
92
  "netlify:ui:deploy": "npx netlify-cli deploy --dir=./packages/ui-components/storybook-static --prod",
91
- "start": "storybook dev -p 6006",
93
+ "start": "BROWSER=none storybook dev -p 6006",
92
94
  "build-storybook": "storybook build"
93
95
  }
94
96
  }
@@ -1,32 +1,89 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { HttpResponse, http } from 'msw';
1
3
  import React from 'react';
2
4
  import { MemoryRouter } from 'react-router';
3
5
 
4
6
  import AppRoute from './AppRoute';
5
7
 
6
- export default {
8
+ const meta: Meta<typeof AppRoute> = {
7
9
  title: 'App/Main',
10
+ component: AppRoute,
8
11
  };
9
12
 
10
- export const ApplicationStoryBook: any = () => (
11
- <MemoryRouter initialEntries={[`/-/web/detail/storybook`]}>
12
- <AppRoute />
13
- </MemoryRouter>
14
- );
15
-
16
- export const ApplicationJquery: any = () => (
17
- <MemoryRouter initialEntries={[`/-/web/detail/jquery`]}>
18
- <AppRoute />
19
- </MemoryRouter>
20
- );
21
-
22
- export const ApplicationForbidden: any = () => (
23
- <MemoryRouter initialEntries={[`/-/web/detail/JSONStream`]}>
24
- <AppRoute />
25
- </MemoryRouter>
26
- );
27
-
28
- export const ApplicationNotFound: any = () => (
29
- <MemoryRouter initialEntries={[`/-/web/detail/kleur`]}>
30
- <AppRoute />
31
- </MemoryRouter>
32
- );
13
+ export default meta;
14
+
15
+ type Story = StoryObj<typeof AppRoute>;
16
+
17
+ export const ApplicationStoryBook: Story = {
18
+ render: () => (
19
+ <MemoryRouter initialEntries={[`/-/web/detail/storybook`]}>
20
+ <AppRoute />
21
+ </MemoryRouter>
22
+ ),
23
+ parameters: {
24
+ msw: {
25
+ handlers: [
26
+ http.get('https://my-registry.org/-/verdaccio/data/sidebar/storybook', () => {
27
+ return HttpResponse.json(require('../../vitest/api/storybook-sidebar.json'));
28
+ }),
29
+ http.get('https://my-registry.org/-/verdaccio/data/package/readme/storybook', () => {
30
+ return HttpResponse.json(require('../../vitest/api/storybook-readme')());
31
+ }),
32
+ ],
33
+ },
34
+ },
35
+ };
36
+
37
+ export const ApplicationJquery: Story = {
38
+ render: () => (
39
+ <MemoryRouter initialEntries={[`/-/web/detail/jquery`]}>
40
+ <AppRoute />
41
+ </MemoryRouter>
42
+ ),
43
+ parameters: {
44
+ msw: {
45
+ handlers: [
46
+ http.get('https://my-registry.org/-/verdaccio/data/sidebar/jquery', () => {
47
+ return HttpResponse.json(require('../../vitest/api/jquery-sidebar.json'));
48
+ }),
49
+ http.get('https://my-registry.org/-/verdaccio/data/package/readme/jquery', () => {
50
+ return HttpResponse.json(require('../../vitest/api/jquery-readme')());
51
+ }),
52
+ ],
53
+ },
54
+ },
55
+ };
56
+
57
+ export const ApplicationForbidden: Story = {
58
+ render: () => (
59
+ <MemoryRouter initialEntries={[`/-/web/detail/JSONStream`]}>
60
+ <AppRoute />
61
+ </MemoryRouter>
62
+ ),
63
+ parameters: {
64
+ msw: {
65
+ handlers: [
66
+ http.get('https://my-registry.org/-/verdaccio/data/sidebar/JSONStream', () => {
67
+ return new HttpResponse('unauthorized', { status: 401 });
68
+ }),
69
+ ],
70
+ },
71
+ },
72
+ };
73
+
74
+ export const ApplicationNotFound: Story = {
75
+ render: () => (
76
+ <MemoryRouter initialEntries={[`/-/web/detail/kleur`]}>
77
+ <AppRoute />
78
+ </MemoryRouter>
79
+ ),
80
+ parameters: {
81
+ msw: {
82
+ handlers: [
83
+ http.get('https://my-registry.org/-/verdaccio/data/sidebar/kleur', () => {
84
+ return new HttpResponse('unauthorized', { status: 404 });
85
+ }),
86
+ ],
87
+ },
88
+ },
89
+ };
@@ -34,13 +34,17 @@ describe('AppRoute', () => {
34
34
 
35
35
  test('renders VersionPage component for PACKAGE path', async () => {
36
36
  act(() => appTest('/-/web/detail/jquery'));
37
- await waitFor(() => screen.getByTestId('readme-tab'));
37
+ await waitFor(() => expect(screen.getByTestId('readme-tab')).toBeInTheDocument(), {
38
+ timeout: 3000,
39
+ });
38
40
  expect(screen.getByTestId('sidebar')).toBeInTheDocument();
39
41
  });
40
42
 
41
43
  test('renders VersionPage component for PACKAGE VERSION path', async () => {
42
44
  act(() => appTest('/-/web/detail/jquery/v/3.6.3'));
43
- await waitFor(() => screen.getByTestId('readme-tab'));
45
+ await waitFor(() => expect(screen.getByTestId('readme-tab')).toBeInTheDocument(), {
46
+ timeout: 3000,
47
+ });
44
48
  expect(screen.getByTestId('sidebar')).toBeInTheDocument();
45
49
  });
46
50
 
@@ -1,7 +1,14 @@
1
1
  import React from 'react';
2
2
 
3
+ import { cleanupDownloadMocks, setupDownloadMocks } from '../../../vitest/vitestHelpers';
3
4
  import { store } from '../../store/store';
4
- import { cleanup, fireEvent, renderWithStore, screen } from '../../test/test-react-testing-library';
5
+ import {
6
+ cleanup,
7
+ fireEvent,
8
+ renderWithStore,
9
+ screen,
10
+ waitFor,
11
+ } from '../../test/test-react-testing-library';
5
12
  import ActionBar from './ActionBar';
6
13
 
7
14
  const defaultPackageMeta = {
@@ -21,25 +28,37 @@ const defaultPackageMeta = {
21
28
  },
22
29
  };
23
30
 
31
+ beforeAll(() => {
32
+ setupDownloadMocks();
33
+ });
34
+
35
+ afterAll(() => {
36
+ cleanupDownloadMocks();
37
+ });
38
+
24
39
  describe('<ActionBar /> component', () => {
25
40
  afterEach(() => {
26
41
  cleanup();
27
42
  });
28
43
 
29
- test('should render the component in default state', () => {
44
+ test('should render the component in default state', async () => {
30
45
  renderWithStore(<ActionBar packageMeta={defaultPackageMeta} />, store);
31
- expect(screen.getByTestId('download-tarball-btn')).toBeInTheDocument();
32
- expect(screen.getByTestId('BugReportIcon')).toBeInTheDocument();
33
- expect(screen.getByTestId('HomeIcon')).toBeInTheDocument();
46
+ await waitFor(() => {
47
+ expect(screen.getByTestId('download-tarball-btn')).toBeInTheDocument();
48
+ expect(screen.getByTestId('BugReportIcon')).toBeInTheDocument();
49
+ expect(screen.getByTestId('HomeIcon')).toBeInTheDocument();
50
+ });
34
51
  });
35
52
 
36
- test('should not render if data is missing', () => {
53
+ test('should not render if data is missing', async () => {
37
54
  // @ts-ignore - testing with missing data
38
55
  renderWithStore(<ActionBar packageMeta={undefined} />, store);
39
- expect(screen.queryByTestId('HomeIcon')).toBeNull();
56
+ await waitFor(() => {
57
+ expect(screen.queryByTestId('HomeIcon')).toBeNull();
58
+ });
40
59
  });
41
60
 
42
- test('when there is no action bar data', () => {
61
+ test('when there is no action bar data', async () => {
43
62
  const packageMeta = {
44
63
  ...defaultPackageMeta,
45
64
  latest: {
@@ -54,60 +73,80 @@ describe('<ActionBar /> component', () => {
54
73
  };
55
74
 
56
75
  renderWithStore(<ActionBar packageMeta={packageMeta} />, store);
57
- expect(screen.queryByTestId('download-tarball-btn')).not.toBeInTheDocument();
58
- expect(screen.queryByTestId('BugReportIcon')).not.toBeInTheDocument();
59
- expect(screen.queryByTestId('HomeIcon')).not.toBeInTheDocument();
76
+ await waitFor(() => {
77
+ expect(screen.queryByTestId('download-tarball-btn')).not.toBeInTheDocument();
78
+ expect(screen.queryByTestId('BugReportIcon')).not.toBeInTheDocument();
79
+ expect(screen.queryByTestId('HomeIcon')).not.toBeInTheDocument();
80
+ });
60
81
  });
61
82
 
62
- test('when there is a button to download a tarball', () => {
83
+ test('when there is a button to download a tarball', async () => {
63
84
  renderWithStore(<ActionBar packageMeta={defaultPackageMeta} />, store);
64
- expect(screen.getByLabelText('action-bar-action.download-tarball')).toBeTruthy();
85
+ await waitFor(() => {
86
+ expect(screen.getByLabelText('action-bar-action.download-tarball')).toBeTruthy();
87
+ });
65
88
  });
66
89
 
67
- test('when button to download is disabled', () => {
90
+ test('when button to download is disabled', async () => {
68
91
  renderWithStore(
69
92
  <ActionBar packageMeta={defaultPackageMeta} showDownloadTarball={false} />,
70
93
  store
71
94
  );
72
- expect(screen.queryByTestId('download-tarball-btn')).not.toBeInTheDocument();
95
+ await waitFor(() => {
96
+ expect(screen.queryByTestId('download-tarball-btn')).not.toBeInTheDocument();
97
+ });
73
98
  });
74
99
 
75
- test('when there is a button to raw manifest', () => {
100
+ test('when there is a button to raw manifest', async () => {
76
101
  renderWithStore(<ActionBar packageMeta={defaultPackageMeta} showRaw={true} />, store);
77
- expect(screen.getByLabelText('action-bar-action.raw')).toBeTruthy();
102
+ await waitFor(() => {
103
+ expect(screen.getByLabelText('action-bar-action.raw')).toBeTruthy();
104
+ });
78
105
  });
79
106
 
80
107
  test('when click button to raw manifest open a dialog with viewer', async () => {
81
108
  renderWithStore(<ActionBar packageMeta={defaultPackageMeta} showRaw={true} />, store);
82
- expect(screen.queryByTestId('rawViewer--dialog')).toBeFalsy();
109
+ await waitFor(() => {
110
+ expect(screen.queryByTestId('rawViewer--dialog')).toBeFalsy();
111
+ });
83
112
 
84
113
  fireEvent.click(screen.getByLabelText('action-bar-action.raw'));
85
- await screen.findByTestId('rawViewer--dialog');
114
+ await waitFor(() => {
115
+ expect(screen.getByTestId('rawViewer--dialog')).toBeInTheDocument();
116
+ });
86
117
 
87
118
  fireEvent.click(screen.getByTestId('close-raw-viewer'));
88
- await screen.getByLabelText('action-bar-action.raw');
89
-
90
- expect(screen.queryByTestId('rawViewer--dialog')).toBeFalsy();
119
+ await waitFor(() => {
120
+ expect(screen.queryByTestId('rawViewer--dialog')).toBeFalsy();
121
+ });
91
122
  });
92
123
 
93
- test('should not display download tarball button', () => {
124
+ test('should not display download tarball button', async () => {
94
125
  renderWithStore(<ActionBar packageMeta={defaultPackageMeta} showRaw={false} />, store);
95
- expect(screen.queryByLabelText('Download tarball')).toBeFalsy();
126
+ await waitFor(() => {
127
+ expect(screen.queryByLabelText('Download tarball')).toBeFalsy();
128
+ });
96
129
  });
97
130
 
98
131
  test('when click button to download ', async () => {
99
132
  renderWithStore(<ActionBar packageMeta={defaultPackageMeta} showRaw={false} />, store);
100
133
  fireEvent.click(screen.getByTestId('download-tarball-btn'));
101
- await store.getState().loading.models.download;
134
+ await waitFor(() => {
135
+ expect(store.getState().loading.models.download).toBe(true);
136
+ });
102
137
  });
103
138
 
104
- test('should not display show raw button', () => {
139
+ test('should not display show raw button', async () => {
105
140
  renderWithStore(<ActionBar packageMeta={defaultPackageMeta} showRaw={false} />, store);
106
- expect(screen.queryByLabelText('action-bar-action.raw')).toBeFalsy();
141
+ await waitFor(() => {
142
+ expect(screen.queryByLabelText('action-bar-action.raw')).toBeFalsy();
143
+ });
107
144
  });
108
145
 
109
- test('when there is a button to open an issue', () => {
146
+ test('when there is a button to open an issue', async () => {
110
147
  renderWithStore(<ActionBar packageMeta={defaultPackageMeta} />, store);
111
- expect(screen.getByTestId('BugReportIcon')).toBeInTheDocument();
148
+ await waitFor(() => {
149
+ expect(screen.getByTestId('BugReportIcon')).toBeInTheDocument();
150
+ });
112
151
  });
113
152
  });
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import { MemoryRouter } from 'react-router';
3
3
 
4
4
  import { store } from '../../';
5
+ import { cleanupDownloadMocks, setupDownloadMocks } from '../../../vitest/vitestHelpers';
5
6
  import {
6
7
  cleanup,
7
8
  fireEvent,
@@ -34,6 +35,14 @@ const props = {
34
35
  keywords: ['verdaccio-core'],
35
36
  };
36
37
 
38
+ beforeAll(() => {
39
+ setupDownloadMocks();
40
+ });
41
+
42
+ afterAll(() => {
43
+ cleanupDownloadMocks();
44
+ });
45
+
37
46
  describe('<Package /> component', () => {
38
47
  afterEach(() => {
39
48
  cleanup();
@@ -1,15 +1,22 @@
1
1
  /* eslint-disable verdaccio/jsx-spread */
2
2
  import Box from '@mui/material/Box';
3
+ import type { Meta, StoryObj } from '@storybook/react';
4
+ import { HttpResponse, http } from 'msw';
3
5
  import React from 'react';
4
6
  import { MemoryRouter } from 'react-router';
5
7
 
8
+ import Home from '../../sections/Home';
6
9
  import Search from './Search';
7
10
 
8
- export default {
11
+ type Story = StoryObj<typeof Search>;
12
+ const meta: Meta<typeof Search> = {
9
13
  title: 'Components/Header/Search',
14
+ component: Home,
10
15
  };
11
16
 
12
- export const SearchByQuery = {
17
+ export default meta;
18
+
19
+ export const SearchByQuery: Story = {
13
20
  render: (args) => (
14
21
  <MemoryRouter initialEntries={[`/-/web/detail/storybook`]}>
15
22
  <Box
@@ -27,4 +34,19 @@ export const SearchByQuery = {
27
34
  </Box>
28
35
  </MemoryRouter>
29
36
  ),
37
+ parameters: {
38
+ msw: {
39
+ handlers: [
40
+ http.get('https://my-registry.org/-/verdaccio/data/sidebar/storybook', () => {
41
+ return HttpResponse.json(require('../../../vitest/api/storybook-sidebar.json'));
42
+ }),
43
+ http.get('https://my-registry.org/-/verdaccio/data/package/readme/storybook', () => {
44
+ return HttpResponse.json(require('../../../vitest/api/storybook-readme')());
45
+ }),
46
+ http.get('https://my-registry.org/-/verdaccio/data/search/*', () => {
47
+ return HttpResponse.json(require('../../../vitest/api/search-verdaccio.json'));
48
+ }),
49
+ ],
50
+ },
51
+ },
30
52
  };
@@ -118,6 +118,8 @@ const Search: React.FC<RouteComponentProps> = ({ history }) => {
118
118
  };
119
119
 
120
120
  const renderOption = (props, option) => {
121
+ const { key, ...otherProps } = props;
122
+
121
123
  if (searchRemote) {
122
124
  const item: SearchResultWeb = option.package;
123
125
  const isPrivate = option?.verdaccioPrivate;
@@ -125,7 +127,8 @@ const Search: React.FC<RouteComponentProps> = ({ history }) => {
125
127
  const isRemote = !isCached && !isPrivate;
126
128
  return (
127
129
  <SearchItem
128
- {...props}
130
+ key={key}
131
+ {...otherProps}
129
132
  description={item?.description}
130
133
  isCached={isCached}
131
134
  isPrivate={isPrivate}
@@ -137,7 +140,8 @@ const Search: React.FC<RouteComponentProps> = ({ history }) => {
137
140
  } else {
138
141
  return (
139
142
  <SearchItem
140
- {...props}
143
+ key={key}
144
+ {...otherProps}
141
145
  description={option?.description}
142
146
  name={option?.name}
143
147
  version={option?.version}
@@ -161,4 +165,4 @@ const Search: React.FC<RouteComponentProps> = ({ history }) => {
161
165
  );
162
166
  };
163
167
 
164
- export default withRouter(Search);
168
+ export default withRouter(Search as any);
@@ -1,4 +1,5 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react';
2
+ import { HttpResponse, http } from 'msw';
2
3
  import React from 'react';
3
4
  import { MemoryRouter, Route } from 'react-router';
4
5
 
@@ -11,6 +12,7 @@ const meta: Meta<typeof VersionLayout> = {
11
12
  };
12
13
 
13
14
  export default meta;
15
+
14
16
  type Story = StoryObj<typeof VersionLayout>;
15
17
 
16
18
  export const Primary: Story = {
@@ -26,6 +28,18 @@ export const Primary: Story = {
26
28
  </MemoryRouter>
27
29
  );
28
30
  },
31
+ parameters: {
32
+ msw: {
33
+ handlers: [
34
+ http.get('https://my-registry.org/-/verdaccio/data/sidebar/storybook', () => {
35
+ return HttpResponse.json(require('../../../vitest/api/storybook-sidebar.json'));
36
+ }),
37
+ http.get('https://my-registry.org/-/verdaccio/data/package/readme/storybook', () => {
38
+ return HttpResponse.json(require('../../../vitest/api/storybook-readme')());
39
+ }),
40
+ ],
41
+ },
42
+ },
29
43
  };
30
44
 
31
45
  export const jQuery: Story = {
@@ -41,4 +55,16 @@ export const jQuery: Story = {
41
55
  </MemoryRouter>
42
56
  );
43
57
  },
58
+ parameters: {
59
+ msw: {
60
+ handlers: [
61
+ http.get('https://my-registry.org/-/verdaccio/data/sidebar/jquery', () => {
62
+ return HttpResponse.json(require('../../../vitest/api/jquery-sidebar.json'));
63
+ }),
64
+ http.get('https://my-registry.org/-/verdaccio/data/package/readme/jquery', () => {
65
+ return HttpResponse.json(require('../../../vitest/api/jquery-readme')());
66
+ }),
67
+ ],
68
+ },
69
+ },
44
70
  };
@@ -1,25 +1,21 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { HttpResponse, http } from 'msw';
1
3
  import React from 'react';
2
4
  import { MemoryRouter, Route } from 'react-router';
3
5
 
4
6
  import { VersionProvider } from '../../providers';
5
7
  import Detail from './Detail';
6
8
 
7
- export default {
9
+ const meta: Meta<typeof Detail> = {
8
10
  title: 'Sections/Detail',
11
+ component: Detail,
9
12
  };
10
13
 
11
- export const DetailStorybook: any = () => (
12
- <MemoryRouter initialEntries={[`/-/web/detail/storybook`]}>
13
- <Route exact={true} path="/-/web/detail/:package">
14
- <VersionProvider>
15
- <Detail />
16
- </VersionProvider>
17
- </Route>
18
- </MemoryRouter>
19
- );
14
+ export default meta;
20
15
 
21
- export const DetailJquery: any = () => (
22
- <MemoryRouter initialEntries={[`/-/web/detail/jquery`]}>
16
+ type Story = StoryObj<typeof Detail>;
17
+ const getDetailApp = (url: string) => (
18
+ <MemoryRouter initialEntries={[url]}>
23
19
  <Route exact={true} path="/-/web/detail/:package">
24
20
  <VersionProvider>
25
21
  <Detail />
@@ -28,12 +24,47 @@ export const DetailJquery: any = () => (
28
24
  </MemoryRouter>
29
25
  );
30
26
 
31
- export const DetailForbidden: any = () => (
32
- <MemoryRouter initialEntries={[`/-/web/detail/JSONStream`]}>
33
- <Route exact={true} path="/-/web/detail/:package">
34
- <VersionProvider>
35
- <Detail />
36
- </VersionProvider>
37
- </Route>
38
- </MemoryRouter>
39
- );
27
+ export const DetailStorybook: Story = {
28
+ render: () => getDetailApp(`/-/web/detail/storybook`),
29
+ parameters: {
30
+ msw: {
31
+ handlers: [
32
+ http.get('https://my-registry.org/-/verdaccio/data/sidebar/storybook', () => {
33
+ return HttpResponse.json(require('../../../vitest/api/storybook-sidebar.json'));
34
+ }),
35
+ http.get('https://my-registry.org/-/verdaccio/data/package/readme/storybook', () => {
36
+ return HttpResponse.json(require('../../../vitest/api/storybook-readme')());
37
+ }),
38
+ ],
39
+ },
40
+ },
41
+ };
42
+
43
+ export const DetailJquery: Story = {
44
+ render: () => getDetailApp(`/-/web/detail/jquery`),
45
+ parameters: {
46
+ msw: {
47
+ handlers: [
48
+ http.get('https://my-registry.org/-/verdaccio/data/sidebar/jquery', () => {
49
+ return HttpResponse.json(require('../../../vitest/api/jquery-sidebar.json'));
50
+ }),
51
+ http.get('https://my-registry.org/-/verdaccio/data/package/readme/jquery', () => {
52
+ return HttpResponse.json(require('../../../vitest/api/jquery-readme')());
53
+ }),
54
+ ],
55
+ },
56
+ },
57
+ };
58
+
59
+ export const DetailForbidden: Story = {
60
+ render: () => getDetailApp(`/-/web/detail/JSONStream`),
61
+ parameters: {
62
+ msw: {
63
+ handlers: [
64
+ http.get('https://my-registry.org/-/verdaccio/data/sidebar/JSONStream', () => {
65
+ return new HttpResponse('unauthorized', { status: 401 });
66
+ }),
67
+ ],
68
+ },
69
+ },
70
+ };
@@ -1,3 +1,5 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { HttpResponse, http } from 'msw';
1
3
  import React from 'react';
2
4
  import { MemoryRouter, Route } from 'react-router';
3
5
 
@@ -5,10 +7,14 @@ import { HeaderInfoDialog } from '../../';
5
7
  import { VersionProvider } from '../../providers';
6
8
  import Header from './Header';
7
9
 
8
- export default {
10
+ type Story = StoryObj<typeof Header>;
11
+ const meta: Meta<typeof Header> = {
9
12
  title: 'Sections/Header',
13
+ component: Header,
10
14
  };
11
15
 
16
+ export default meta;
17
+
12
18
  function CustomInfoDialog({ onCloseDialog, title, isOpen }) {
13
19
  return (
14
20
  <HeaderInfoDialog
@@ -25,12 +31,29 @@ function CustomInfoDialog({ onCloseDialog, title, isOpen }) {
25
31
  );
26
32
  }
27
33
 
28
- export const HeaderAll: any = () => (
29
- <MemoryRouter initialEntries={[`/-/web/detail/storybook`]}>
30
- <Route exact={true} path="/-/web/detail/:package">
31
- <VersionProvider>
32
- <Header HeaderInfoDialog={CustomInfoDialog} />
33
- </VersionProvider>
34
- </Route>
35
- </MemoryRouter>
36
- );
34
+ export const HeaderAll: Story = {
35
+ render: () => (
36
+ <MemoryRouter initialEntries={[`/-/web/detail/storybook`]}>
37
+ <Route exact={true} path="/-/web/detail/:package">
38
+ <VersionProvider>
39
+ <Header HeaderInfoDialog={CustomInfoDialog} />
40
+ </VersionProvider>
41
+ </Route>
42
+ </MemoryRouter>
43
+ ),
44
+ parameters: {
45
+ msw: {
46
+ handlers: [
47
+ http.get('https://my-registry.org/-/verdaccio/data/sidebar/storybook', () => {
48
+ return HttpResponse.json(require('../../../vitest/api/storybook-sidebar.json'));
49
+ }),
50
+ http.get('https://my-registry.org/-/verdaccio/data/package/readme/storybook', () => {
51
+ return HttpResponse.json(require('../../../vitest/api/storybook-readme')());
52
+ }),
53
+ http.get('https://my-registry.org/-/verdaccio/data/search/*', () => {
54
+ return HttpResponse.json(require('../../../vitest/api/search-verdaccio.json'));
55
+ }),
56
+ ],
57
+ },
58
+ },
59
+ };
@@ -1,16 +1,33 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { HttpResponse, http } from 'msw';
1
3
  import React from 'react';
2
4
  import { MemoryRouter, Route } from 'react-router';
3
5
 
4
6
  import Home from './Home';
5
7
 
6
- export default {
8
+ type Story = StoryObj<typeof Home>;
9
+ const meta: Meta<typeof Home> = {
7
10
  title: 'Sections/Home',
11
+ component: Home,
8
12
  };
9
13
 
10
- export const HomeDefault: any = () => (
11
- <MemoryRouter initialEntries={[`/`]}>
12
- <Route exact={true} path="/">
13
- <Home />
14
- </Route>
15
- </MemoryRouter>
16
- );
14
+ export default meta;
15
+
16
+ export const HomeDefault: Story = {
17
+ render: () => (
18
+ <MemoryRouter initialEntries={[`/`]}>
19
+ <Route exact={true} path="/">
20
+ <Home />
21
+ </Route>
22
+ </MemoryRouter>
23
+ ),
24
+ parameters: {
25
+ msw: {
26
+ handlers: [
27
+ http.get('https://my-registry.org/-/verdaccio/data/packages', () => {
28
+ return HttpResponse.json([...require('../../../vitest/api/home.json')]);
29
+ }),
30
+ ],
31
+ },
32
+ },
33
+ };
@@ -1,39 +1,64 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { HttpResponse, http } from 'msw';
1
3
  import React from 'react';
2
4
  import { MemoryRouter, Route } from 'react-router';
3
5
 
4
6
  import { VersionProvider } from '../../providers';
5
7
  import DetailSidebar from './Sidebar';
6
8
 
7
- export default {
9
+ const meta: Meta<typeof DetailSidebar> = {
8
10
  title: 'Sections/Sidebar',
11
+ component: DetailSidebar,
9
12
  };
10
13
 
11
- export const SidebarLatestPackage: any = () => (
12
- <MemoryRouter initialEntries={[`/-/web/detail/storybook`]}>
13
- <Route exact={true} path="/-/web/detail/:package">
14
- <VersionProvider>
15
- <DetailSidebar />
16
- </VersionProvider>
17
- </Route>
18
- </MemoryRouter>
19
- );
14
+ export default meta;
20
15
 
21
- export const SidebarPackageVersion: any = () => (
22
- <MemoryRouter initialEntries={[`/-/web/detail/storybook/v/6.0.26`]}>
23
- <Route exact={true} path="/-/web/detail/:package/v/:version">
24
- <VersionProvider>
25
- <DetailSidebar />
26
- </VersionProvider>
27
- </Route>
28
- </MemoryRouter>
29
- );
16
+ type Story = StoryObj<typeof DetailSidebar>;
30
17
 
31
- export const SidebarNoFounding: any = () => (
32
- <MemoryRouter initialEntries={[`/-/web/detail/jquery`]}>
33
- <Route exact={true} path="/-/web/detail/:package">
34
- <VersionProvider>
35
- <DetailSidebar />
36
- </VersionProvider>
37
- </Route>
38
- </MemoryRouter>
39
- );
18
+ export const SidebarLatestPackage: Story = {
19
+ render: () => (
20
+ <MemoryRouter initialEntries={[`/-/web/detail/storybook`]}>
21
+ <Route exact={true} path="/-/web/detail/:package">
22
+ <VersionProvider>
23
+ <DetailSidebar />
24
+ </VersionProvider>
25
+ </Route>
26
+ </MemoryRouter>
27
+ ),
28
+ parameters: {
29
+ msw: {
30
+ handlers: [
31
+ http.get('https://my-registry.org/-/verdaccio/data/sidebar/storybook', () => {
32
+ return HttpResponse.json(require('../../../vitest/api/storybook-sidebar.json'));
33
+ }),
34
+ http.get('https://my-registry.org/-/verdaccio/data/package/readme/storybook', () => {
35
+ return HttpResponse.json(require('../../../vitest/api/storybook-readme')());
36
+ }),
37
+ ],
38
+ },
39
+ },
40
+ };
41
+
42
+ export const SidebarPackageVersion: Story = {
43
+ render: () => (
44
+ <MemoryRouter initialEntries={[`/-/web/detail/storybook/v/6.0.26`]}>
45
+ <Route exact={true} path="/-/web/detail/:package/v/:version">
46
+ <VersionProvider>
47
+ <DetailSidebar />
48
+ </VersionProvider>
49
+ </Route>
50
+ </MemoryRouter>
51
+ ),
52
+ parameters: {
53
+ msw: {
54
+ handlers: [
55
+ http.get('https://my-registry.org/-/verdaccio/data/sidebar/storybook', () => {
56
+ return HttpResponse.json(require('../../../vitest/api/storybook-sidebar.json'));
57
+ }),
58
+ http.get('https://my-registry.org/-/verdaccio/data/package/readme/storybook', () => {
59
+ return HttpResponse.json(require('../../../vitest/api/storybook-readme')());
60
+ }),
61
+ ],
62
+ },
63
+ },
64
+ };
package/src/utils/url.ts CHANGED
@@ -45,13 +45,29 @@ export function downloadFile(fileStream: Blob, fileName: string): void {
45
45
  fileLink.href = objectURL;
46
46
  fileLink.download = fileName;
47
47
 
48
- // Without appending to an HTML Element, download dialog does not show up on Firefox
49
- // https://github.com/verdaccio/ui/issues/119
50
- document.documentElement.appendChild(fileLink);
51
- fileLink.click();
52
- // firefox requires remove the object url
53
- setTimeout(() => {
54
- URL.revokeObjectURL(objectURL);
55
- document.documentElement.removeChild(fileLink);
56
- }, 150);
48
+ // Skip actual download in test environment to avoid JSDOM navigation error
49
+ // Check if we're in a test environment (JSDOM)
50
+ const isTestEnvironment =
51
+ (typeof window !== 'undefined' &&
52
+ window.navigator &&
53
+ window.navigator.userAgent &&
54
+ window.navigator.userAgent.includes('Node.js')) ||
55
+ window.navigator.userAgent.includes('jsdom');
56
+
57
+ if (!isTestEnvironment) {
58
+ // Without appending to an HTML Element, download dialog does not show up on Firefox
59
+ // https://github.com/verdaccio/ui/issues/119
60
+ document.documentElement.appendChild(fileLink);
61
+ fileLink.click();
62
+ // firefox requires remove the object url
63
+ setTimeout(() => {
64
+ URL.revokeObjectURL(objectURL);
65
+ document.documentElement.removeChild(fileLink);
66
+ }, 150);
67
+ } else {
68
+ // In test environment, just clean up without triggering navigation
69
+ setTimeout(() => {
70
+ URL.revokeObjectURL(objectURL);
71
+ }, 150);
72
+ }
57
73
  }
@@ -10,10 +10,10 @@
10
10
  "rootDir": ".",
11
11
  "outDir": "./build",
12
12
  "skipLibCheck": true,
13
- "jsx": "react"
13
+ "jsx": "react",
14
+ "types": ["node", "@testing-library/jest-dom"]
14
15
  },
15
16
  "include": ["src/**/*", "./src/types/index.d.ts"],
16
- "types": ["node", "@testing-library/jest-dom"],
17
17
  "typedocOptions": {
18
18
  "categoryOrder": ["Model", "Component", "Provider", "*"],
19
19
  "defaultCategory": "Component",
package/tsconfig.json CHANGED
@@ -10,9 +10,10 @@
10
10
  "outDir": "./build",
11
11
  "preserveSymlinks": true,
12
12
  "jsx": "react",
13
- "allowJs": true
13
+ "allowJs": true,
14
+ "types": ["node", "jest", "@testing-library/jest-dom"]
14
15
  },
15
- "types": ["node", "jest", "@testing-library/jest-dom"],
16
+ "include": ["src/**/*", "vitest/**/*", "src/test/**/*"],
16
17
  "references": [
17
18
  {
18
19
  "path": "../core/types"
package/vitest.config.mjs CHANGED
@@ -7,7 +7,7 @@ export default defineConfig({
7
7
  environment: 'jsdom',
8
8
  globals: true,
9
9
  setupFiles: ['./vitest/setup.ts', './vitest/setup-env.ts'],
10
- exclude: ['node_modules', './build/'],
10
+ exclude: ['**/node_modules/**', '**/build/**'],
11
11
  snapshotFormat: {
12
12
  escapeString: true,
13
13
  printBasicPrototype: true,
@@ -22,7 +22,7 @@ export default defineConfig({
22
22
  '\\.(jpg)$': './vitest/unit/empty.ts',
23
23
  '\\.(md)$': './vitest/unit/empty-string.ts',
24
24
  },
25
- testTimeout: 15000,
25
+ testTimeout: 20000,
26
26
  },
27
27
  plugins: [
28
28
  react({