Repository: tjtanjin/react-chatbotify (ts)

Issue:
[Task] Add unit test cases for FileAttachmentButton component
**Task Description:**
Currently, we run unit tests using jest which can be found [**here**](https://github.com/tjtanjin/react-chatbotify/tree/main/__tests__). 

However, the test cases are not yet comprehensive enough and work can be done to cover more grounds. Specifically for this issue, we hope to add test cases for the `FileAttachmentButton` component.

**Deliverable(s):**
New unit test cases that cover the testing of the `FileAttachmentButton` component.

**Additional Context:**
- Button components can be found [**here**](https://github.com/tjtanjin/react-chatbotify/tree/main/src/components/Buttons)
- An example for `AudioButton` can be found [**here**](https://github.com/tjtanjin/react-chatbotify/blob/main/__tests__/components/AudioButton.test.tsx) which may serve as a good source for reference


Pull request:
Added FileAttachmentButton test
#### Description

Added unit test for FileAttachmentButton component

Closes #141 

#### What change does this PR introduce?

Please select the relevant option(s).

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Documentation update (changes to docs/code comments)

#### What is the proposed approach?

The proposed approach focuses on validating the behavior and rendering of the `FileAttachmentButton` component, ensuring that it works as expected based on its context and props. This includes:

1. **Rendering Verification**:

- Confirming that the button and input elements are correctly rendered with the expected attributes, such as `aria-label`, `type`, `accept`, and `multiple`.

2. **State-Dependent Icon Rendering**:

- Testing that the button’s enabled and disabled states render the correct classes and icons based on the `blockAllowsAttachment` context value.

3. **Preventing Invalid Actions**:

- Verifying that the `handleUpload` function does not proceed when certain conditions are unmet, such as when `getCurrPath` returns `null`.

4. **File Upload Handling**:

- Ensuring that the `handleUpload` function triggers the correct callbacks and processes file uploads as expected, including calling a mocked file handler with the appropriate arguments.

#### Checklist:

- [x] The commit message follows our adopted [guidelines](https://www.conventionalcommits.org/en/v1.0.0/)
- [x] Testing has been done for the change(s) added (for bug fixes/features)
- [ ] Relevant comments/docs have been added/updated (for bug fixes/features)

Patch:
diff --git a/package-lock.json b/package-lock.json
index 638763f..b9d3af7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3892,9 +3892,10 @@
       }
     },
     "node_modules/cross-spawn": {
-      "version": "7.0.3",
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "path-key": "^3.1.0",
         "shebang-command": "^2.0.0",
diff --git a/src/components/Buttons/FileAttachmentButton/FileAttachmentButton.tsx b/src/components/Buttons/FileAttachmentButton/FileAttachmentButton.tsx
index 2323dbc..992f8d1 100644
--- a/src/components/Buttons/FileAttachmentButton/FileAttachmentButton.tsx
+++ b/src/components/Buttons/FileAttachmentButton/FileAttachmentButton.tsx
@@ -167,6 +167,7 @@ const FileAttachmentButton = () => {
 		>
 			<input
 				className="rcb-attach-input"
+				role="file"
 				type="file"
 				onChange={handleUpload}
 				multiple={settings.fileAttachment?.multiple}


Test patch:
diff --git a/__tests__/components/buttons/FileAttachmentbutton.test.tsx b/__tests__/components/buttons/FileAttachmentbutton.test.tsx
new file mode 100644
index 0000000..e6f1834
--- /dev/null
+++ b/__tests__/components/buttons/FileAttachmentbutton.test.tsx
@@ -0,0 +1,198 @@
+import React from "react";
+import { render, screen, fireEvent } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import FileAttachmentButton from "../../../src/components/Buttons/FileAttachmentButton/FileAttachmentButton";
+import { TestChatBotProvider } from "../../__mocks__/TestChatBotContext";
+import { useMessagesInternal } from "../../../src/hooks/internal/useMessagesInternal";
+import { useSubmitInputInternal } from "../../../src/hooks/internal/useSubmitInputInternal";
+import { usePathsInternal } from "../../../src/hooks/internal/usePathsInternal";
+import { getMediaFileDetails } from "../../../src/utils/mediaFileParser";
+import { useBotRefsContext } from "../../../src/context/BotRefsContext";
+
+jest.mock("../../../src/hooks/internal/useMessagesInternal", () => ({
+	useMessagesInternal: jest.fn(),
+}));
+jest.mock("../../../src/hooks/internal/useSubmitInputInternal", () => ({
+	useSubmitInputInternal: jest.fn(),
+}));
+jest.mock("../../../src/hooks/internal/usePathsInternal", () => ({
+	usePathsInternal: jest.fn(),
+}));
+jest.mock("../../../src/utils/mediaFileParser", () => ({
+	getMediaFileDetails: jest.fn(),
+}));
+jest.mock("../../../src/context/BotRefsContext", () => ({
+	...jest.requireActual("../../../src/context/BotRefsContext"),
+	BotRefsProvider: ({ children }: { children: React.ReactNode }) => (
+		<div data-testid="bot-refs-provider">{children}</div>
+	),
+	useBotRefsContext: jest.fn(() => ({
+		botIdRef: { current: "testBotId" },
+		flowRef: { current: { currentPath: { file: jest.fn() } } },
+	})),
+}));
+
+/**
+ * Helper function to render FileAttachmentButton with specific initial settings.
+ * @param blockAllowsAttachment Whether the block allows attachment
+ * @param showMediaDisplay Whether media display is enabled
+ */
+const renderFileAttachmentButton = (blockAllowsAttachment: boolean, showMediaDisplay: boolean) => {
+	const initialSettings = {
+		fileAttachment: {
+			sendFileName: true,
+			multiple: true,
+			accept: ".jpg,.png",
+			showMediaDisplay: showMediaDisplay,
+			icon: "attachment-icon-url",
+			iconDisabled: "attachment-icon-disabled-url",
+		},
+		general: {
+			actionDisabledIcon: "disabled-icon-url",
+		},
+		ariaLabel: {
+			fileAttachmentButton: "upload file",
+		},
+	};
+	const initialStyles = {
+		fileAttachmentButtonStyle: { cursor: "pointer" },
+		fileAttachmentButtonDisabledStyle: { cursor: "not-allowed" },
+		fileAttachmentIconStyle: { backgroundImage: "url(icon.png)" },
+		fileAttachmentIconDisabledStyle: { backgroundImage: "url(disabled-icon.png)" },
+	};
+
+	return render(
+		<TestChatBotProvider initialSettings={initialSettings} initialStyles={initialStyles}>
+			<FileAttachmentButton />
+		</TestChatBotProvider>
+	);
+};
+
+describe("FileAttachmentButton Component", () => {
+	const mockInjectMessage = jest.fn();
+	const mockHandleSubmitText = jest.fn();
+  
+	beforeEach(() => {
+		jest.clearAllMocks();
+  
+		(useMessagesInternal as jest.Mock).mockReturnValue({
+			injectMessage: mockInjectMessage,
+		});
+  
+		(useSubmitInputInternal as jest.Mock).mockReturnValue({
+			handleSubmitText: mockHandleSubmitText,
+		});
+
+		(usePathsInternal as jest.Mock).mockReturnValue({
+			blockAllowsAttachment: true,
+			getCurrPath: jest.fn().mockReturnValue("currentPath"),
+			getPrevPath: jest.fn().mockReturnValue("previousPath"),
+			goToPath: jest.fn(),
+		});
+
+		(getMediaFileDetails as jest.Mock).mockResolvedValue({
+			fileType: "image",
+			fileUrl: "http://example.com/test.png",
+		});
+	});
+  
+	it("renders with correct aria-label and initial state when attachment is allowed", () => {
+		renderFileAttachmentButton(true, true);
+  
+		const button = screen.getByRole("button", { name: "upload file" });
+		expect(button).toBeInTheDocument();
+		expect(button).toHaveClass("rcb-attach-button-enabled");
+  
+		const input = screen.getByRole("file");
+		expect(input).toBeEnabled();
+		expect(input).toHaveAttribute("type", "file");
+		expect(input).toHaveAttribute("multiple");
+		expect(input).toHaveAttribute("accept", ".jpg,.png");
+	});
+  
+	it("renders with disabled state when attachment is not allowed", () => {
+		(usePathsInternal as jest.Mock).mockReturnValueOnce({
+			blockAllowsAttachment: false,
+			getCurrPath: jest.fn(),
+			getPrevPath: jest.fn(),
+			goToPath: jest.fn(),
+		});
+		renderFileAttachmentButton(false, true);
+  
+		const input = screen.getByRole("file");
+		expect(input).toBeDisabled();
+  
+		const button = screen.getByRole("button", { name: "upload file" });
+		expect(button).toHaveClass("rcb-attach-button-disabled");
+	});
+
+	it("displays correct icon for enabled state", () => {
+		renderFileAttachmentButton(true, true);
+    
+		const icon = screen.getByRole("button", { name: "upload file" });
+		expect(icon).toBeInTheDocument();
+		expect(icon).toHaveClass("rcb-attach-button-enabled");
+	});
+
+	it("displays correct icon for disabled state", () => {
+		(usePathsInternal as jest.Mock).mockReturnValueOnce({
+			blockAllowsAttachment: false,
+			getCurrPath: jest.fn(),
+			getPrevPath: jest.fn(),
+			goToPath: jest.fn(),
+		});
+    
+		renderFileAttachmentButton(false, true);
+    
+		const icon = screen.getByRole("button", { name: "upload file" });
+		expect(icon).toBeInTheDocument();
+		expect(icon).toHaveClass("rcb-attach-button-disabled");
+	});
+
+	it("does not proceed when getCurrPath returns null", () => {
+		(usePathsInternal as jest.Mock).mockReturnValueOnce({
+			blockAllowsAttachment: true,
+			getCurrPath: jest.fn().mockReturnValue(null),
+			getPrevPath: jest.fn(),
+			goToPath: jest.fn(),
+		});
+      
+		renderFileAttachmentButton(true, true);
+      
+		const input = screen.getByRole("file");
+		const file = new File(["test content"], "test.png", { type: "image/png" });
+      
+		fireEvent.change(input, { target: { files: [file] } });
+      
+		expect(mockHandleSubmitText).not.toHaveBeenCalled();
+		expect(mockInjectMessage).not.toHaveBeenCalled();
+	});
+
+	it("handles file upload and calls the appropriate functions", async () => {
+		const mockFileHandler = jest.fn();
+		const mockFlow = { currentPath: { file: mockFileHandler } };
+    
+		(useBotRefsContext as jest.Mock).mockReturnValueOnce({
+			...jest.requireMock("../../../src/context/BotRefsContext").useBotRefsContext(),
+			flowRef: { current: mockFlow },
+			inputRef: { current: { value: "test input" } },
+		});
+    
+		renderFileAttachmentButton(true, true);
+    
+		const input = screen.getByRole("file");
+		const file = new File(["test content"], "test.png", { type: "image/png" });
+    
+		fireEvent.change(input, { target: { files: [file] } });
+    
+		await new Promise((resolve) => setTimeout(resolve, 0));
+    
+		expect(mockFileHandler).toHaveBeenCalled();
+		expect(mockFileHandler).toHaveBeenCalledWith(
+			expect.objectContaining({
+				userInput: "test input",
+				files: [file],
+			})
+		);
+	});
+});