diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/File/AttachmentComponents/AttachmentPlaceholder.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/File/AttachmentComponents/AttachmentPlaceholder.test.tsx
new file mode 100644
index 00000000000..c4a4ef5e6ae
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/File/AttachmentComponents/AttachmentPlaceholder.test.tsx
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2025 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { cleanup, render, screen } from '@testing-library/react';
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import { getFileIcon } from '../../../../../utils/BlockEditorUtils';
+import { FileType } from '../../../BlockEditor.interface';
+import AttachmentPlaceholder from './AttachmentPlaceholder';
+
+// Mock the translation hook
+jest.mock('react-i18next', () => ({
+ useTranslation: jest.fn(),
+}));
+
+// Mock the getFileIcon utility to return a valid React component
+jest.mock('../../../../../utils/BlockEditorUtils', () => ({
+ getFileIcon: jest
+ .fn()
+ .mockReturnValue(() =>
),
+}));
+
+describe('AttachmentPlaceholder', () => {
+ const mockTranslate = jest.fn();
+
+ beforeEach(() => {
+ // Reset all mocks before each test
+ jest.clearAllMocks();
+
+ // Setup translation mock
+ (useTranslation as jest.Mock).mockReturnValue({
+ t: mockTranslate,
+ });
+
+ // Setup getFileIcon mock
+ (getFileIcon as jest.Mock).mockReturnValue(() => (
+
+ ));
+ });
+
+ afterEach(() => {
+ cleanup();
+ });
+
+ it('should render the placeholder with correct file type', () => {
+ const fileType = FileType.FILE;
+ mockTranslate.mockImplementation((key, options) => {
+ if (key === 'label.add-an-file-type') {
+ return `Add a ${options.fileType} file`;
+ }
+ if (key === `label.${fileType}`) {
+ return fileType;
+ }
+
+ return key;
+ });
+
+ render();
+
+ // Verify the placeholder is rendered
+ expect(screen.getByTestId('image-placeholder')).toBeInTheDocument();
+
+ // Verify the icon is rendered
+ expect(screen.getByTestId('mock-file-icon')).toBeInTheDocument();
+
+ // Verify the translation was called with correct parameters
+ expect(mockTranslate).toHaveBeenCalledWith('label.add-an-file-type', {
+ fileType: expect.any(String),
+ });
+ });
+
+ it.each([
+ [FileType.FILE, 'file'],
+ [FileType.IMAGE, 'image'],
+ [FileType.VIDEO, 'video'],
+ ])('should render with %s file type', (fileType, expectedText) => {
+ mockTranslate.mockImplementation((key, options) => {
+ if (key === 'label.add-an-file-type') {
+ return `Add a ${options.fileType} file`;
+ }
+ if (key === `label.${fileType}`) {
+ return fileType;
+ }
+
+ return key;
+ });
+
+ render();
+
+ expect(screen.getByTestId('image-placeholder')).toBeInTheDocument();
+ expect(screen.getByTestId('mock-file-icon')).toBeInTheDocument();
+ expect(screen.getByText(`Add a ${expectedText} file`)).toBeInTheDocument();
+ });
+
+ it('should have contentEditable set to false', () => {
+ const fileType = FileType.FILE;
+ mockTranslate.mockImplementation((key, options) => {
+ if (key === 'label.add-an-file-type') {
+ return `Add a ${options.fileType} file`;
+ }
+ if (key === `label.${fileType}`) {
+ return fileType;
+ }
+
+ return key;
+ });
+
+ render();
+
+ const placeholder = screen.getByTestId('image-placeholder');
+
+ expect(placeholder).toHaveAttribute('contentEditable', 'false');
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/File/AttachmentComponents/FileAttachment.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/File/AttachmentComponents/FileAttachment.test.tsx
new file mode 100644
index 00000000000..30f57857b8b
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/File/AttachmentComponents/FileAttachment.test.tsx
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2025 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { fireEvent, render, screen } from '@testing-library/react';
+import { NodeViewProps } from '@tiptap/react';
+import React from 'react';
+import { bytesToSize } from '../../../../../utils/StringsUtils';
+import FileAttachment from './FileAttachment';
+
+describe('FileAttachment', () => {
+ // Create a minimal mock that only includes what the component needs
+ const createMockNode = (attrs: unknown) =>
+ ({
+ attrs: {
+ url: '',
+ fileName: '',
+ fileSize: 0,
+ mimeType: '',
+ isUploading: false,
+ uploadProgress: 0,
+ tempFile: null,
+ ...(attrs as NodeViewProps['node']['attrs']),
+ },
+ } as unknown as NodeViewProps['node']); // Type assertion to avoid TipTap Node type complexity
+
+ const mockNode = createMockNode({
+ url: 'https://example.com/file.pdf',
+ fileName: 'test.pdf',
+ fileSize: 1024 * 1024, // 1MB
+ mimeType: 'application/pdf',
+ });
+
+ const mockProps = {
+ node: mockNode,
+ isFileLoading: false,
+ deleteNode: jest.fn(),
+ onFileClick: jest.fn(),
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders file attachment with correct details', () => {
+ render();
+
+ // Check if file name is displayed
+ expect(screen.getByText('test.pdf')).toBeInTheDocument();
+
+ // Check if file size is displayed correctly
+ expect(screen.getByText(bytesToSize(1024 * 1024))).toBeInTheDocument();
+
+ // Check if download button is present
+ expect(screen.getByRole('button')).toBeInTheDocument();
+ });
+
+ it('handles file click correctly', () => {
+ render();
+
+ const fileLink = screen.getByText('test.pdf');
+ fireEvent.click(fileLink);
+
+ expect(mockProps.onFileClick).toHaveBeenCalled();
+ });
+
+ it('handles delete button click correctly', () => {
+ render();
+
+ const deleteButton = screen.getByLabelText('delete');
+ fireEvent.click(deleteButton);
+
+ expect(mockProps.deleteNode).toHaveBeenCalled();
+ });
+
+ it('shows upload progress when file is uploading', () => {
+ const uploadingNode = createMockNode({
+ ...mockNode.attrs,
+ isUploading: true,
+ uploadProgress: 50,
+ });
+
+ render();
+
+ // Check if upload progress is displayed
+ const progressBar = screen.getByTestId('upload-progress');
+
+ expect(progressBar).toHaveStyle({ width: '50%' });
+
+ // Check if delete button is not present during upload
+ expect(screen.queryByLabelText('delete')).not.toBeInTheDocument();
+ });
+
+ it('shows loading state on download button when isFileLoading is true', () => {
+ render();
+
+ const downloadButton = screen.getByRole('button');
+
+ expect(downloadButton).toHaveClass('ant-btn-loading');
+ });
+
+ it('uses tempFile details when available', () => {
+ const tempFileNode = createMockNode({
+ ...mockNode.attrs,
+ tempFile: {
+ name: 'temp.pdf',
+ size: 2048 * 1024, // 2MB
+ type: 'application/pdf',
+ },
+ fileName: null,
+ fileSize: null,
+ mimeType: null,
+ });
+
+ render();
+
+ // Check if temp file name is displayed
+ expect(screen.getByText('temp.pdf')).toBeInTheDocument();
+
+ // Check if temp file size is displayed correctly
+ expect(screen.getByText(bytesToSize(2048 * 1024))).toBeInTheDocument();
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/File/AttachmentComponents/FileAttachment.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/File/AttachmentComponents/FileAttachment.tsx
index 8008727b948..27b7c0eceba 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/File/AttachmentComponents/FileAttachment.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/File/AttachmentComponents/FileAttachment.tsx
@@ -16,11 +16,9 @@ import {
FileOutlined,
} from '@ant-design/icons';
import { NodeViewProps } from '@tiptap/react';
-import { Spin } from 'antd';
+import { Button } from 'antd';
import React from 'react';
-import { useTranslation } from 'react-i18next';
import { bytesToSize } from '../../../../../utils/StringsUtils';
-import Loader from '../../../../common/Loader/Loader';
const FileAttachment = ({
node,
@@ -33,8 +31,6 @@ const FileAttachment = ({
deleteNode: () => void;
onFileClick: (e: React.MouseEvent) => void;
}) => {
- const { t } = useTranslation();
-
const {
url,
fileName,
@@ -46,57 +42,58 @@ const FileAttachment = ({
} = node.attrs;
return (
- }
- spinning={isFileLoading || isUploading}
- tip={isUploading ? t('label.uploading') : t('label.loading')}>
-