dhre-component-lib 0.0.1
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/jest.config.ts +10 -0
- package/package.json +47 -0
- package/rollup.config.mjs +37 -0
- package/setupTests.ts +1 -0
- package/src/__mocks__/styleMock.js +1 -0
- package/src/components/Avatar/Avatar.tsx +26 -0
- package/src/components/Avatar/index.ts +1 -0
- package/src/components/Badge/Badge.css +15 -0
- package/src/components/Badge/Badge.tsx +25 -0
- package/src/components/Badge/index.ts +1 -0
- package/src/components/BreadCrumb/BreadCrumb.tsx +36 -0
- package/src/components/BreadCrumb/Breadcrumb.css +21 -0
- package/src/components/BreadCrumb/index.ts +0 -0
- package/src/components/Button/Button.css +92 -0
- package/src/components/Button/Button.tsx +22 -0
- package/src/components/Button/index.ts +1 -0
- package/src/components/Checkbox/Checkbox.test.tsx +61 -0
- package/src/components/Checkbox/Checkbox.tsx +35 -0
- package/src/components/Checkbox/index.ts +1 -0
- package/src/components/Circular_Progress/CircularProgress.css +19 -0
- package/src/components/Circular_Progress/CircularProgress.test.tsx +47 -0
- package/src/components/Circular_Progress/CircularProgress.tsx +37 -0
- package/src/components/Circular_Progress/index.ts +1 -0
- package/src/components/Divider/Divider.test.tsx +19 -0
- package/src/components/Divider/Divider.tsx +24 -0
- package/src/components/Divider/index.ts +1 -0
- package/src/components/Enum.ts +19 -0
- package/src/components/InputTextField/InputTextField.test.tsx +48 -0
- package/src/components/InputTextField/InputTextField.tsx +46 -0
- package/src/components/InputTextField/index.ts +1 -0
- package/src/components/Link/Link.test.tsx +47 -0
- package/src/components/Link/Link.tsx +33 -0
- package/src/components/Link/index.ts +1 -0
- package/src/components/Modal/Modal.test.tsx +51 -0
- package/src/components/Modal/Modal.tsx +19 -0
- package/src/components/Modal/index.ts +1 -0
- package/src/components/Notification/Notification.css +20 -0
- package/src/components/Notification/Notification.test.tsx +53 -0
- package/src/components/Notification/Notification.tsx +42 -0
- package/src/components/Notification/index.ts +1 -0
- package/src/components/PdfView/PdfView.test.tsx +79 -0
- package/src/components/PdfView/PdfView.tsx +93 -0
- package/src/components/PdfView/index.ts +1 -0
- package/src/components/Progress/Progress.test.tsx +35 -0
- package/src/components/Progress/Progress.tsx +35 -0
- package/src/components/Progress/index.ts +1 -0
- package/src/components/RadioButton/RadioButton.test.tsx +38 -0
- package/src/components/RadioButton/RadioButton.tsx +43 -0
- package/src/components/RadioButton/index.ts +1 -0
- package/src/components/Switch/Switch.tsx +38 -0
- package/src/components/Switch/index.ts +1 -0
- package/src/components/Tag/Tag.css +15 -0
- package/src/components/Tag/Tag.tsx +25 -0
- package/src/components/Tag/index.ts +1 -0
- package/src/components/Tooltip/Tooltip.css +38 -0
- package/src/components/Tooltip/Tooltip.tsx +34 -0
- package/src/components/Tooltip/index.ts +1 -0
- package/src/components/index.ts +11 -0
- package/src/index.ts +1 -0
- package/tsconfig.json +121 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import InputTextField from "./InputTextField";
|
|
4
|
+
|
|
5
|
+
describe("InputTextField Component", () => {
|
|
6
|
+
it("should render without crashing", () => {
|
|
7
|
+
const { getByTestId } = render(
|
|
8
|
+
<InputTextField label="Name" data-testid="inputTextField-rendered" />
|
|
9
|
+
);
|
|
10
|
+
expect(getByTestId("inputTextField-rendered")).toBeInTheDocument();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should render with default props", () => {
|
|
14
|
+
const { getByLabelText } = render(<InputTextField label="Name" />);
|
|
15
|
+
const inputElement = getByLabelText("Name") as HTMLInputElement;
|
|
16
|
+
|
|
17
|
+
expect(inputElement).toHaveAttribute("type", "text");
|
|
18
|
+
|
|
19
|
+
expect(inputElement).toHaveClass("MuiInputBase-input");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should render with the specified type", () => {
|
|
23
|
+
const { getByLabelText } = render(
|
|
24
|
+
<InputTextField label="Password" type="password" />
|
|
25
|
+
);
|
|
26
|
+
const inputElement = getByLabelText("Password") as HTMLInputElement;
|
|
27
|
+
|
|
28
|
+
expect(inputElement).toHaveAttribute("type", "password");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should render with the specified variant", () => {
|
|
32
|
+
const { getByLabelText } = render(
|
|
33
|
+
<InputTextField label="Email" variant="outlined" />
|
|
34
|
+
);
|
|
35
|
+
const inputElement = getByLabelText("Email") as HTMLInputElement;
|
|
36
|
+
|
|
37
|
+
expect(inputElement).toHaveClass("MuiOutlinedInput-input");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should render with helper text", () => {
|
|
41
|
+
const { getByText } = render(
|
|
42
|
+
<InputTextField label="Email" helperText="Enter your email" />
|
|
43
|
+
);
|
|
44
|
+
const helperTextElement = getByText("Enter your email");
|
|
45
|
+
|
|
46
|
+
expect(helperTextElement).toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { INPUT_TYPES } from '../Enum';
|
|
3
|
+
|
|
4
|
+
export interface CustomInputFieldProps {
|
|
5
|
+
label?: string;
|
|
6
|
+
value?: string;
|
|
7
|
+
type?:string ;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
onChange?: React.ChangeEventHandler<HTMLInputElement>;
|
|
10
|
+
className?: string;
|
|
11
|
+
inputClassName?: string;
|
|
12
|
+
labelClassName?: string;
|
|
13
|
+
error?: string;
|
|
14
|
+
errorClassName?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const CustomInputField: React.FC<CustomInputFieldProps> = ({
|
|
18
|
+
label,
|
|
19
|
+
value = '',
|
|
20
|
+
type = INPUT_TYPES.TEXT,
|
|
21
|
+
placeholder = '',
|
|
22
|
+
onChange,
|
|
23
|
+
className = '',
|
|
24
|
+
inputClassName = '',
|
|
25
|
+
labelClassName = '',
|
|
26
|
+
error = '',
|
|
27
|
+
errorClassName = '',
|
|
28
|
+
...rest
|
|
29
|
+
}) => {
|
|
30
|
+
return (
|
|
31
|
+
<div className={className}>
|
|
32
|
+
{label && <label className={labelClassName}>{label}</label>}
|
|
33
|
+
<input
|
|
34
|
+
type={type}
|
|
35
|
+
value={value}
|
|
36
|
+
placeholder={placeholder}
|
|
37
|
+
onChange={onChange}
|
|
38
|
+
className={inputClassName}
|
|
39
|
+
{...rest}
|
|
40
|
+
/>
|
|
41
|
+
{error && <div className={errorClassName}>{error}</div>}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default CustomInputField;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./InputTextField";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, fireEvent } from "@testing-library/react";
|
|
3
|
+
import Link from "./Link";
|
|
4
|
+
|
|
5
|
+
describe("Link Component", () => {
|
|
6
|
+
it("should render without crashing", () => {
|
|
7
|
+
const { getByRole } = render(
|
|
8
|
+
<Link href="https://example.com">Test Link</Link>
|
|
9
|
+
);
|
|
10
|
+
const linkElement = getByRole("link");
|
|
11
|
+
expect(linkElement).toBeInTheDocument();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("should render with default props", () => {
|
|
15
|
+
const { getByRole } = render(<Link href="https://example.com" />);
|
|
16
|
+
const linkElement = getByRole("link");
|
|
17
|
+
|
|
18
|
+
expect(linkElement).toHaveAttribute("href", "https://example.com");
|
|
19
|
+
|
|
20
|
+
expect(linkElement).toHaveAttribute("target", "_self");
|
|
21
|
+
|
|
22
|
+
expect(linkElement).toHaveAttribute("rel", "noopener noreferrer");
|
|
23
|
+
|
|
24
|
+
expect(linkElement).toHaveClass("MuiLink-underlineNone");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should render with target="_blank"', () => {
|
|
28
|
+
const { getByRole } = render(
|
|
29
|
+
<Link href="https://example.com" target="_blank" />
|
|
30
|
+
);
|
|
31
|
+
const linkElement = getByRole("link");
|
|
32
|
+
|
|
33
|
+
expect(linkElement).toHaveAttribute("target", "_blank");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should handle onClick event", () => {
|
|
37
|
+
const handleClick = jest.fn();
|
|
38
|
+
const { getByRole } = render(
|
|
39
|
+
<Link href="https://example.com" onClick={handleClick} />
|
|
40
|
+
);
|
|
41
|
+
const linkElement = getByRole("link");
|
|
42
|
+
|
|
43
|
+
fireEvent.click(linkElement);
|
|
44
|
+
|
|
45
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { LINK_TARGET } from '../Enum';
|
|
3
|
+
|
|
4
|
+
interface LinkProps {
|
|
5
|
+
href: string;
|
|
6
|
+
target?:string ;
|
|
7
|
+
rel?: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
children?: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const Link: React.FC<LinkProps> = ({
|
|
13
|
+
href,
|
|
14
|
+
target = LINK_TARGET.Self,
|
|
15
|
+
rel,
|
|
16
|
+
className = '',
|
|
17
|
+
children,
|
|
18
|
+
...rest
|
|
19
|
+
}) => {
|
|
20
|
+
return (
|
|
21
|
+
<a
|
|
22
|
+
href={href}
|
|
23
|
+
target={target}
|
|
24
|
+
rel={rel}
|
|
25
|
+
className={className}
|
|
26
|
+
{...rest}
|
|
27
|
+
>
|
|
28
|
+
{children}
|
|
29
|
+
</a>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default Link;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./Link";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import Modal from "./Modal";
|
|
4
|
+
|
|
5
|
+
describe("Modal Component", () => {
|
|
6
|
+
test("renders children when open", () => {
|
|
7
|
+
render(
|
|
8
|
+
<Modal isOpen={true} onClose={() => {}}>
|
|
9
|
+
<div>Modal Content</div>
|
|
10
|
+
</Modal>
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
expect(screen.getByText("Modal Content")).toBeInTheDocument();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("does not render children when not open", () => {
|
|
17
|
+
render(
|
|
18
|
+
<Modal isOpen={false} onClose={() => {}}>
|
|
19
|
+
<div>Modal Content</div>
|
|
20
|
+
</Modal>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
expect(screen.queryByText("Modal Content")).not.toBeInTheDocument();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("calls onClose when the modal is closed", () => {
|
|
27
|
+
const onCloseMock = jest.fn();
|
|
28
|
+
|
|
29
|
+
render(
|
|
30
|
+
<Modal isOpen={true} onClose={onCloseMock}>
|
|
31
|
+
<div>Modal Content</div>
|
|
32
|
+
</Modal>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// Manually trigger the close event
|
|
36
|
+
onCloseMock();
|
|
37
|
+
|
|
38
|
+
expect(onCloseMock).toHaveBeenCalledTimes(1);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("applies the className to the modal container", () => {
|
|
42
|
+
render(
|
|
43
|
+
<Modal isOpen={true} onClose={() => {}} className="custom-class">
|
|
44
|
+
<div>Modal Content</div>
|
|
45
|
+
</Modal>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const modalContainer = screen.getByText("Modal Content").parentElement;
|
|
49
|
+
expect(modalContainer).toHaveClass("custom-class");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Modal as MuiModal } from "@mui/material";
|
|
3
|
+
|
|
4
|
+
interface ModalProps {
|
|
5
|
+
isOpen: boolean;
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const Modal: React.FC<ModalProps> = ({ isOpen, onClose, children, className, ...rest }) => {
|
|
12
|
+
return (
|
|
13
|
+
<MuiModal open={isOpen} onClose={onClose} {...rest}>
|
|
14
|
+
<div className={className}>{children}</div>
|
|
15
|
+
</MuiModal>
|
|
16
|
+
);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default Modal;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./Modal";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
.notification {
|
|
2
|
+
position: fixed;
|
|
3
|
+
bottom: 20px;
|
|
4
|
+
left: 20px;
|
|
5
|
+
width: 300px;
|
|
6
|
+
padding: 16px;
|
|
7
|
+
border-radius: 4px;
|
|
8
|
+
display: flex;
|
|
9
|
+
justify-content: space-between;
|
|
10
|
+
align-items: center;
|
|
11
|
+
cursor: pointer;
|
|
12
|
+
z-index: 9999;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.notification .close-button {
|
|
16
|
+
background: none;
|
|
17
|
+
border: none;
|
|
18
|
+
font-size: 20px;
|
|
19
|
+
cursor: pointer;
|
|
20
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
3
|
+
import Notification, { NotificationProps } from './Notification';
|
|
4
|
+
|
|
5
|
+
describe('Notification Component', () => {
|
|
6
|
+
const defaultProps: NotificationProps = {
|
|
7
|
+
message: 'Test Message',
|
|
8
|
+
severity: 'info',
|
|
9
|
+
autoHideDuration: 6000,
|
|
10
|
+
onClose: jest.fn(),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const renderNotification = (props: Partial<NotificationProps> = {}) => {
|
|
14
|
+
return render(<Notification {...defaultProps} {...props} />);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
test('renders notification with the correct message', () => {
|
|
18
|
+
render(<Notification message="Test Message" severity="info" />);
|
|
19
|
+
|
|
20
|
+
// Check if the element exists by querying it
|
|
21
|
+
const notificationElement = screen.getByText('Test Message');
|
|
22
|
+
expect(notificationElement).not.toBeNull(); // Equivalent to `toBeInTheDocument`
|
|
23
|
+
|
|
24
|
+
// Alternatively, check for the presence of the role or className
|
|
25
|
+
const alertElement = screen.getByRole('alert');
|
|
26
|
+
expect(alertElement.className).toContain('info'); // Check if className includes "info"
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('calls onClose when the notification is clicked', () => {
|
|
30
|
+
const onClose = jest.fn();
|
|
31
|
+
renderNotification({ onClose });
|
|
32
|
+
|
|
33
|
+
fireEvent.click(screen.getByRole('alert'));
|
|
34
|
+
|
|
35
|
+
expect(onClose).toHaveBeenCalled();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('calls onClose when the close button is clicked', () => {
|
|
39
|
+
const onClose = jest.fn();
|
|
40
|
+
renderNotification({ onClose });
|
|
41
|
+
|
|
42
|
+
fireEvent.click(screen.getByLabelText('Close notification'));
|
|
43
|
+
|
|
44
|
+
expect(onClose).toHaveBeenCalledWith(expect.anything(), 'close');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('calls onClose after autoHideDuration expires', async () => {
|
|
48
|
+
const onClose = jest.fn();
|
|
49
|
+
renderNotification({ onClose, autoHideDuration: 1000 });
|
|
50
|
+
|
|
51
|
+
await waitFor(() => expect(onClose).toHaveBeenCalledWith(expect.anything(), 'timeout'), { timeout: 1500 });
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
|
+
import './Notification.css';
|
|
3
|
+
export interface NotificationProps {
|
|
4
|
+
message: string;
|
|
5
|
+
severity?: 'error' | 'warning' | 'info' | 'success';
|
|
6
|
+
autoHideDuration?: number;
|
|
7
|
+
onClose?: (event: React.SyntheticEvent<any> | Event, reason?: string) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const Notification: React.FC<NotificationProps> = ({
|
|
11
|
+
message,
|
|
12
|
+
severity = 'info',
|
|
13
|
+
autoHideDuration = 6000,
|
|
14
|
+
onClose,
|
|
15
|
+
}) => {
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (autoHideDuration && onClose) {
|
|
18
|
+
const timer = setTimeout(() => {
|
|
19
|
+
onClose(new Event('timeout'), 'timeout');
|
|
20
|
+
}, autoHideDuration);
|
|
21
|
+
|
|
22
|
+
return () => clearTimeout(timer);
|
|
23
|
+
}
|
|
24
|
+
}, [autoHideDuration, onClose]);
|
|
25
|
+
|
|
26
|
+
const handleClose = (event: React.SyntheticEvent<any> | Event, reason?: string) => {
|
|
27
|
+
if (onClose) {
|
|
28
|
+
onClose(event, reason);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className={`notification ${severity}`} onClick={(e) => handleClose(e)} role="alert">
|
|
34
|
+
<div className="notification-message">{message}</div>
|
|
35
|
+
<button className="close-button" onClick={(e) => handleClose(e, 'close')} aria-label="Close notification">
|
|
36
|
+
×
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default Notification;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./Notification";
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
import PdfView, { PdfViewerProps } from './PdfView';
|
|
5
|
+
|
|
6
|
+
// Mock the URL.createObjectURL and URL.revokeObjectURL functions
|
|
7
|
+
global.URL.createObjectURL = jest.fn();
|
|
8
|
+
global.URL.revokeObjectURL = jest.fn();
|
|
9
|
+
|
|
10
|
+
const defaultProps: PdfViewerProps = {
|
|
11
|
+
content: 'mockBase64Data',
|
|
12
|
+
buttonText: 'View PDF',
|
|
13
|
+
showLoadingSpinner: true, // Set this to true to test the spinner
|
|
14
|
+
loadingText: 'Loading...',
|
|
15
|
+
errorText: 'Failed to load PDF',
|
|
16
|
+
spinnerSize: 24,
|
|
17
|
+
spinnerColor: 'primary',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
describe('PdfView Component', () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
jest.clearAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should render the PDF viewer with the default button text', () => {
|
|
26
|
+
render(<PdfView {...defaultProps} />);
|
|
27
|
+
expect(screen.getByText('View PDF')).toBeInTheDocument();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
it('should render an error message if the PDF fails to load', async () => {
|
|
32
|
+
// Override the base64ToBlob method to throw an error
|
|
33
|
+
jest.spyOn(global, 'atob').mockImplementation(() => {
|
|
34
|
+
throw new Error('Error converting base64');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
render(<PdfView {...defaultProps} />);
|
|
38
|
+
|
|
39
|
+
// Click the button to trigger the error
|
|
40
|
+
fireEvent.click(screen.getByText('View PDF'));
|
|
41
|
+
|
|
42
|
+
// Wait for the error message to appear
|
|
43
|
+
await waitFor(() => expect(screen.getByText('Failed to load PDF')).toBeInTheDocument());
|
|
44
|
+
|
|
45
|
+
jest.restoreAllMocks();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should call the onError callback when there is an error', async () => {
|
|
49
|
+
const mockOnError = jest.fn();
|
|
50
|
+
|
|
51
|
+
// Simulate an error in base64 to blob conversion
|
|
52
|
+
jest.spyOn(global, 'atob').mockImplementation(() => {
|
|
53
|
+
throw new Error('Error converting base64');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
render(<PdfView {...defaultProps} onError={mockOnError} />);
|
|
57
|
+
|
|
58
|
+
// Click the button to trigger the error
|
|
59
|
+
fireEvent.click(screen.getByText('View PDF'));
|
|
60
|
+
|
|
61
|
+
// Wait for the error callback to be called
|
|
62
|
+
await waitFor(() => expect(mockOnError).toHaveBeenCalled());
|
|
63
|
+
|
|
64
|
+
jest.restoreAllMocks();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should open the PDF in a new tab when the button is clicked', async () => {
|
|
68
|
+
render(<PdfView {...defaultProps} />);
|
|
69
|
+
|
|
70
|
+
// Click the button to trigger PDF opening
|
|
71
|
+
fireEvent.click(screen.getByText('View PDF'));
|
|
72
|
+
|
|
73
|
+
// Check if URL.createObjectURL was called
|
|
74
|
+
await waitFor(() => expect(global.URL.createObjectURL).toHaveBeenCalled());
|
|
75
|
+
|
|
76
|
+
// Check if window.open was called
|
|
77
|
+
expect(global.URL.createObjectURL).toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Button, Box, CircularProgress, Typography } from '@mui/material';
|
|
3
|
+
|
|
4
|
+
export interface PdfViewerProps {
|
|
5
|
+
content: string;
|
|
6
|
+
contentType?: string;
|
|
7
|
+
buttonText?: string;
|
|
8
|
+
loadingText?: string;
|
|
9
|
+
errorText?: string;
|
|
10
|
+
onError?: (error: Error) => void;
|
|
11
|
+
cleanUpDelay?: number;
|
|
12
|
+
buttonVariant?: 'text' | 'outlined' | 'contained';
|
|
13
|
+
buttonColor?: 'inherit' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning'; // Removed "default"
|
|
14
|
+
buttonSize?: 'small' | 'medium' | 'large';
|
|
15
|
+
showLoadingSpinner?: boolean;
|
|
16
|
+
spinnerSize?: number;
|
|
17
|
+
spinnerColor?: 'inherit' | 'primary' | 'secondary';
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const PdfView: React.FC<PdfViewerProps> = ({
|
|
22
|
+
content,
|
|
23
|
+
contentType = "application/pdf",
|
|
24
|
+
buttonText = "View PDF",
|
|
25
|
+
loadingText = "Loading...",
|
|
26
|
+
errorText = "Failed to load PDF",
|
|
27
|
+
onError,
|
|
28
|
+
cleanUpDelay = 1000,
|
|
29
|
+
buttonVariant = "contained",
|
|
30
|
+
buttonColor = "primary", // Changed to a valid default
|
|
31
|
+
buttonSize = "medium",
|
|
32
|
+
showLoadingSpinner = false,
|
|
33
|
+
spinnerSize = 24,
|
|
34
|
+
spinnerColor = "primary",
|
|
35
|
+
className,
|
|
36
|
+
}) => {
|
|
37
|
+
const [isLoading, setIsLoading] = React.useState(false);
|
|
38
|
+
const [isError, setIsError] = React.useState(false);
|
|
39
|
+
|
|
40
|
+
const handleViewPdf = async () => {
|
|
41
|
+
setIsLoading(true);
|
|
42
|
+
setIsError(false);
|
|
43
|
+
try {
|
|
44
|
+
const blob = base64ToBlob(content, contentType);
|
|
45
|
+
const url = URL.createObjectURL(blob);
|
|
46
|
+
|
|
47
|
+
window.open(url, '_blank');
|
|
48
|
+
|
|
49
|
+
setTimeout(() => {
|
|
50
|
+
URL.revokeObjectURL(url);
|
|
51
|
+
}, cleanUpDelay);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
setIsError(true);
|
|
54
|
+
if (onError) {
|
|
55
|
+
onError(error as Error);
|
|
56
|
+
} else {
|
|
57
|
+
console.error("Error generating PDF URL:", error);
|
|
58
|
+
}
|
|
59
|
+
} finally {
|
|
60
|
+
setIsLoading(false);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Box className={className} display="flex" flexDirection="column" alignItems="center" justifyContent="center">
|
|
66
|
+
{isLoading && showLoadingSpinner && <CircularProgress size={spinnerSize} color={spinnerColor} />}
|
|
67
|
+
{isError && <Typography color="error">{errorText}</Typography>}
|
|
68
|
+
{!isLoading && !isError && (
|
|
69
|
+
<Button
|
|
70
|
+
onClick={handleViewPdf}
|
|
71
|
+
variant={buttonVariant}
|
|
72
|
+
color={buttonColor} // Ensure only valid options are passed
|
|
73
|
+
size={buttonSize}
|
|
74
|
+
>
|
|
75
|
+
{buttonText}
|
|
76
|
+
</Button>
|
|
77
|
+
)}
|
|
78
|
+
{isLoading && <Typography>{loadingText}</Typography>}
|
|
79
|
+
</Box>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const base64ToBlob = (content: string, contentType: string = 'application/pdf'): Blob => {
|
|
84
|
+
const byteCharacters = atob(content);
|
|
85
|
+
const byteNumbers = new Array(byteCharacters.length);
|
|
86
|
+
for (let i = 0; i < byteCharacters.length; i++) {
|
|
87
|
+
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
88
|
+
}
|
|
89
|
+
const byteArray = new Uint8Array(byteNumbers);
|
|
90
|
+
return new Blob([byteArray], { type: contentType });
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export default PdfView;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./PdfView";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import Progress from "./Progress";
|
|
4
|
+
|
|
5
|
+
describe("Progress Component", () => {
|
|
6
|
+
it("should render without crashing", () => {
|
|
7
|
+
render(<Progress value={50} />);
|
|
8
|
+
const progressElement = screen.getByRole("progressbar");
|
|
9
|
+
expect(progressElement).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("should render with default props", () => {
|
|
13
|
+
render(<Progress value={50} />);
|
|
14
|
+
const progressElement = screen.getByRole("progressbar");
|
|
15
|
+
expect(progressElement).toHaveClass("MuiLinearProgress-colorPrimary");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should render with custom variant", () => {
|
|
19
|
+
render(<Progress value={50} variant="determinate" />);
|
|
20
|
+
const progressElement = screen.getByRole("progressbar");
|
|
21
|
+
expect(progressElement).toHaveClass("MuiLinearProgress-determinate");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should render with custom color", () => {
|
|
25
|
+
render(<Progress value={50} color="secondary" />);
|
|
26
|
+
const progressElement = screen.getByRole("progressbar");
|
|
27
|
+
expect(progressElement).toHaveClass("MuiLinearProgress-colorSecondary");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should apply custom size", () => {
|
|
31
|
+
render(<Progress value={50} size={150} />);
|
|
32
|
+
const progressElement = screen.getByRole("progressbar");
|
|
33
|
+
expect(progressElement).toHaveStyle({ width: "150px", height: "150px" });
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface ProgressProps {
|
|
4
|
+
value: number;
|
|
5
|
+
max: number;
|
|
6
|
+
className?: string;
|
|
7
|
+
barContainerClassName?: string;
|
|
8
|
+
barClassName?: string;
|
|
9
|
+
labelClassName?: string;
|
|
10
|
+
label?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const Progress: React.FC<ProgressProps> = ({
|
|
14
|
+
value,
|
|
15
|
+
max,
|
|
16
|
+
className = '',
|
|
17
|
+
barContainerClassName = '',
|
|
18
|
+
barClassName = '',
|
|
19
|
+
labelClassName = '',
|
|
20
|
+
label,
|
|
21
|
+
...rest
|
|
22
|
+
}) => {
|
|
23
|
+
const percentage = (value / max) * 100;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className={className} {...rest}>
|
|
27
|
+
{label && <div className={labelClassName}>{label}</div>}
|
|
28
|
+
<div className={barContainerClassName}>
|
|
29
|
+
<div className={barClassName} style={{ width: `${percentage}%` }} />
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default Progress;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./Progress";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
+
import RadioButton from "./RadioButton";
|
|
4
|
+
|
|
5
|
+
describe("RadioButton Component", () => {
|
|
6
|
+
it("should render without crashing", () => {
|
|
7
|
+
render(<RadioButton value="option1" />);
|
|
8
|
+
const radioElement = screen.getByRole("radio");
|
|
9
|
+
expect(radioElement).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("should render with default props", () => {
|
|
13
|
+
render(<RadioButton value="option1" />);
|
|
14
|
+
const radioElement = screen.getByRole("radio");
|
|
15
|
+
expect(radioElement).not.toBeChecked();
|
|
16
|
+
expect(radioElement).toHaveAttribute("value", "option1");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should handle the checked prop", () => {
|
|
20
|
+
render(<RadioButton value="option1" checked />);
|
|
21
|
+
const radioElement = screen.getByRole("radio");
|
|
22
|
+
expect(radioElement).toBeChecked();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should be disabled when disabled prop is true", () => {
|
|
26
|
+
render(<RadioButton value="option1" disabled />);
|
|
27
|
+
const radioElement = screen.getByRole("radio");
|
|
28
|
+
expect(radioElement).toBeDisabled();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should call the onChange handler when clicked", () => {
|
|
32
|
+
const handleChange = jest.fn();
|
|
33
|
+
render(<RadioButton value="option1" onChange={handleChange} />);
|
|
34
|
+
const radioElement = screen.getByRole("radio");
|
|
35
|
+
fireEvent.click(radioElement);
|
|
36
|
+
expect(handleChange).toHaveBeenCalledTimes(1);
|
|
37
|
+
});
|
|
38
|
+
});
|