diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CopyToClipboardButton/CopyToClipboardButton.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/CopyToClipboardButton/CopyToClipboardButton.test.tsx
index 0f3dd1028d1..15ca644976d 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/CopyToClipboardButton/CopyToClipboardButton.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/CopyToClipboardButton/CopyToClipboardButton.test.tsx
@@ -33,6 +33,12 @@ Object.defineProperty(window.navigator, 'clipboard', {
writable: true,
});
+// Set secure context to true by default
+Object.defineProperty(window, 'isSecureContext', {
+ value: true,
+ writable: true,
+});
+
describe('Test CopyToClipboardButton Component', () => {
it('Should render all child elements', async () => {
render();
@@ -45,7 +51,7 @@ describe('Test CopyToClipboardButton Component', () => {
render();
await act(async () => {
- fireEvent.click(screen.getByTestId('copy-secret'));
+ await fireEvent.click(screen.getByTestId('copy-secret'));
});
expect(callBack).toHaveBeenCalled();
diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/useClipBoard.test.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/useClipBoard.test.ts
index b751f5ef203..e083b34e191 100644
--- a/openmetadata-ui/src/main/resources/ui/src/hooks/useClipBoard.test.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useClipBoard.test.ts
@@ -19,8 +19,58 @@ const clipboardMock = {
writeText: clipboardWriteTextMock,
};
-Object.defineProperty(window.navigator, 'clipboard', {
- value: clipboardMock,
+// Mock document.execCommand for fallback testing
+const execCommandMock = jest.fn();
+
+// Mock document.execCommand
+Object.defineProperty(document, 'execCommand', {
+ value: execCommandMock,
+ writable: true,
+});
+
+// Mock document.createElement and related DOM methods
+const createElementMock = jest.fn();
+const appendChildMock = jest.fn();
+const removeChildMock = jest.fn();
+const focusMock = jest.fn();
+const selectMock = jest.fn();
+
+// Create a new mock textarea for each test to avoid state pollution
+const createMockTextArea = () => {
+ const textArea = {
+ value: '',
+ style: {},
+ focus: focusMock,
+ select: selectMock,
+ };
+
+ // Allow value to be set
+ Object.defineProperty(textArea, 'value', {
+ get() {
+ return this._value || '';
+ },
+ set(value) {
+ this._value = value;
+ },
+ configurable: true,
+ });
+
+ return textArea;
+};
+
+createElementMock.mockImplementation(() => createMockTextArea());
+
+// Mock document methods
+Object.defineProperty(document, 'createElement', {
+ value: createElementMock,
+ writable: true,
+});
+
+Object.defineProperty(document, 'body', {
+ value: {
+ appendChild: appendChildMock,
+ removeChild: removeChildMock,
+ },
writable: true,
});
@@ -29,10 +79,47 @@ const callBack = jest.fn();
const timeout = 1000;
describe('useClipboard hook', () => {
+ beforeEach(() => {
+ // Reset all mocks
+ jest.clearAllMocks();
+
+ // Set up default clipboard mock
+ Object.defineProperty(window.navigator, 'clipboard', {
+ value: clipboardMock,
+ writable: true,
+ });
+
+ // Set secure context to true by default
+ Object.defineProperty(window, 'isSecureContext', {
+ value: true,
+ writable: true,
+ });
+
+ // Reset document mocks
+ Object.defineProperty(document, 'createElement', {
+ value: createElementMock,
+ writable: true,
+ });
+
+ Object.defineProperty(document, 'body', {
+ value: {
+ appendChild: appendChildMock,
+ removeChild: removeChildMock,
+ },
+ writable: true,
+ });
+
+ Object.defineProperty(document, 'execCommand', {
+ value: execCommandMock,
+ writable: true,
+ });
+ });
+
afterEach(() => {
jest.clearAllMocks();
});
+ // Original tests
it('Should copy to clipboard', async () => {
clipboardWriteTextMock.mockResolvedValue(value);
const { result } = renderHook(() => useClipboard(value, timeout, callBack));
@@ -80,4 +167,205 @@ describe('useClipboard hook', () => {
expect(result.current.hasCopied).toBe(false);
});
+
+ // New comprehensive tests for fallback functionality
+ it('Should copy to clipboard using modern API when available', async () => {
+ clipboardWriteTextMock.mockResolvedValue(value);
+ const { result } = renderHook(() => useClipboard(value, timeout, callBack));
+
+ await act(async () => {
+ result.current.onCopyToClipBoard();
+ });
+
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith(value);
+ expect(result.current.hasCopied).toBe(true);
+ expect(callBack).toHaveBeenCalled();
+ });
+
+ it('Should use fallback method when modern API is not available', async () => {
+ // Remove clipboard API
+ Object.defineProperty(window.navigator, 'clipboard', {
+ value: undefined,
+ writable: true,
+ });
+
+ execCommandMock.mockReturnValue(true);
+
+ const { result } = renderHook(() => useClipboard(value, timeout, callBack));
+
+ await act(async () => {
+ result.current.onCopyToClipBoard();
+ });
+
+ expect(createElementMock).toHaveBeenCalledWith('textarea');
+ expect(appendChildMock).toHaveBeenCalled();
+ expect(focusMock).toHaveBeenCalled();
+ expect(selectMock).toHaveBeenCalled();
+ expect(execCommandMock).toHaveBeenCalledWith('copy');
+ expect(removeChildMock).toHaveBeenCalled();
+ expect(result.current.hasCopied).toBe(true);
+ expect(callBack).toHaveBeenCalled();
+ });
+
+ it('Should use fallback method when not in secure context', async () => {
+ // Set secure context to false
+ Object.defineProperty(window, 'isSecureContext', {
+ value: false,
+ writable: true,
+ });
+
+ execCommandMock.mockReturnValue(true);
+
+ const { result } = renderHook(() => useClipboard(value, timeout, callBack));
+
+ await act(async () => {
+ result.current.onCopyToClipBoard();
+ });
+
+ expect(createElementMock).toHaveBeenCalledWith('textarea');
+ expect(appendChildMock).toHaveBeenCalled();
+ expect(focusMock).toHaveBeenCalled();
+ expect(selectMock).toHaveBeenCalled();
+ expect(execCommandMock).toHaveBeenCalledWith('copy');
+ expect(removeChildMock).toHaveBeenCalled();
+ expect(result.current.hasCopied).toBe(true);
+ expect(callBack).toHaveBeenCalled();
+ });
+
+ it('Should handle error when modern API fails and fallback succeeds', async () => {
+ clipboardWriteTextMock.mockRejectedValue(new Error('Modern API failed'));
+ execCommandMock.mockReturnValue(true);
+
+ const { result } = renderHook(() => useClipboard(value, timeout, callBack));
+
+ await act(async () => {
+ result.current.onCopyToClipBoard();
+ });
+
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith(value);
+ expect(createElementMock).toHaveBeenCalledWith('textarea');
+ expect(appendChildMock).toHaveBeenCalled();
+ expect(focusMock).toHaveBeenCalled();
+ expect(selectMock).toHaveBeenCalled();
+ expect(execCommandMock).toHaveBeenCalledWith('copy');
+ expect(removeChildMock).toHaveBeenCalled();
+ expect(result.current.hasCopied).toBe(true);
+ expect(callBack).toHaveBeenCalled();
+ });
+
+ it('Should handle error when both modern API and fallback fail', async () => {
+ clipboardWriteTextMock.mockRejectedValue(new Error('Modern API failed'));
+ execCommandMock.mockReturnValue(false);
+
+ const { result } = renderHook(() => useClipboard(value, timeout, callBack));
+
+ await act(async () => {
+ result.current.onCopyToClipBoard();
+ });
+
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith(value);
+ expect(createElementMock).toHaveBeenCalledWith('textarea');
+ expect(appendChildMock).toHaveBeenCalled();
+ expect(focusMock).toHaveBeenCalled();
+ expect(selectMock).toHaveBeenCalled();
+ expect(execCommandMock).toHaveBeenCalledWith('copy');
+ expect(removeChildMock).toHaveBeenCalled();
+ expect(result.current.hasCopied).toBe(false);
+ expect(callBack).not.toHaveBeenCalled();
+ });
+
+ it('Should handle error when fallback throws exception', async () => {
+ // Remove clipboard API
+ Object.defineProperty(window.navigator, 'clipboard', {
+ value: undefined,
+ writable: true,
+ });
+
+ execCommandMock.mockImplementation(() => {
+ throw new Error('execCommand failed');
+ });
+
+ const { result } = renderHook(() => useClipboard(value, timeout, callBack));
+
+ await act(async () => {
+ result.current.onCopyToClipBoard();
+ });
+
+ expect(createElementMock).toHaveBeenCalledWith('textarea');
+ expect(appendChildMock).toHaveBeenCalled();
+ expect(focusMock).toHaveBeenCalled();
+ expect(selectMock).toHaveBeenCalled();
+ expect(execCommandMock).toHaveBeenCalledWith('copy');
+ expect(removeChildMock).toHaveBeenCalled();
+ expect(result.current.hasCopied).toBe(false);
+ expect(callBack).not.toHaveBeenCalled();
+ });
+
+ it('Should handle paste from clipboard using modern API when available', async () => {
+ const mockReadText = jest.fn().mockResolvedValue('Pasted text');
+ Object.defineProperty(window.navigator, 'clipboard', {
+ value: { ...clipboardMock, readText: mockReadText },
+ writable: true,
+ });
+
+ const { result } = renderHook(() => useClipboard(value, timeout, callBack));
+
+ await act(async () => {
+ const pastedText = await result.current.onPasteFromClipBoard();
+
+ expect(pastedText).toBe('Pasted text');
+ });
+
+ expect(mockReadText).toHaveBeenCalled();
+ });
+
+ it('Should return null for paste when modern API is not available', async () => {
+ // Remove clipboard API
+ Object.defineProperty(window.navigator, 'clipboard', {
+ value: undefined,
+ writable: true,
+ });
+
+ const { result } = renderHook(() => useClipboard(value, timeout, callBack));
+
+ await act(async () => {
+ const pastedText = await result.current.onPasteFromClipBoard();
+
+ expect(pastedText).toBeNull();
+ });
+ });
+
+ it('Should return null for paste when not in secure context', async () => {
+ // Set secure context to false
+ Object.defineProperty(window, 'isSecureContext', {
+ value: false,
+ writable: true,
+ });
+
+ const { result } = renderHook(() => useClipboard(value, timeout, callBack));
+
+ await act(async () => {
+ const pastedText = await result.current.onPasteFromClipBoard();
+
+ expect(pastedText).toBeNull();
+ });
+ });
+
+ it('Should update value state when value prop changes', () => {
+ const { result, rerender } = renderHook(
+ ({ value }) => useClipboard(value, timeout, callBack),
+ { initialProps: { value: 'Initial Value' } }
+ );
+
+ expect(result.current.hasCopied).toBe(false);
+
+ rerender({ value: 'Updated Value' });
+
+ // Trigger copy to verify it uses the updated value
+ act(() => {
+ result.current.onCopyToClipBoard();
+ });
+
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith('Updated Value');
+ });
});
diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/useClipBoard.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/useClipBoard.ts
index 042eb064ff7..9a2adcfee11 100644
--- a/openmetadata-ui/src/main/resources/ui/src/hooks/useClipBoard.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useClipBoard.ts
@@ -13,6 +13,36 @@
import { toNumber } from 'lodash';
import { useCallback, useEffect, useState } from 'react';
+/**
+ * Fallback method for copying text to clipboard using document.execCommand
+ * This works in older browsers and doesn't require HTTPS
+ */
+const fallbackCopyTextToClipboard = (text: string): boolean => {
+ const textArea = document.createElement('textarea');
+ textArea.value = text;
+
+ // Avoid scrolling to bottom
+ textArea.style.top = '0';
+ textArea.style.left = '0';
+ textArea.style.position = 'fixed';
+ textArea.style.opacity = '0';
+
+ document.body.appendChild(textArea);
+ textArea.focus();
+ textArea.select();
+
+ try {
+ const successful = document.execCommand('copy');
+ document.body.removeChild(textArea);
+
+ return successful;
+ } catch (err) {
+ document.body.removeChild(textArea);
+
+ return false;
+ }
+};
+
/**
* React hook to copy text to clipboard
* @param value the text to copy
@@ -31,19 +61,49 @@ export const useClipboard = (
// handlers
const handleCopy = useCallback(async () => {
try {
- await navigator.clipboard.writeText(valueState);
- setHasCopied(true);
- callBack && callBack();
+ let success = false;
+ if (navigator.clipboard && window.isSecureContext) {
+ // Try modern clipboard API first
+ await navigator.clipboard.writeText(valueState);
+ success = true;
+ } else {
+ // Fallback for older browsers or non-HTTPS contexts
+ success = fallbackCopyTextToClipboard(valueState);
+ }
+
+ if (success) {
+ setHasCopied(true);
+ callBack && callBack();
+ } else {
+ setHasCopied(false);
+ }
} catch (error) {
- setHasCopied(false);
+ // If modern API fails, try fallback
+ try {
+ const success = fallbackCopyTextToClipboard(valueState);
+ if (success) {
+ setHasCopied(true);
+ callBack && callBack();
+ } else {
+ setHasCopied(false);
+ }
+ } catch (fallbackError) {
+ setHasCopied(false);
+ }
}
- }, [valueState]);
+ }, [valueState, callBack]);
const handlePaste = useCallback(async () => {
try {
- const text = await navigator.clipboard.readText();
+ // Try modern clipboard API first
+ if (navigator.clipboard && window.isSecureContext) {
+ const text = await navigator.clipboard.readText();
- return text;
+ return text;
+ } else {
+ // Fallback for older browsers
+ return null;
+ }
} catch (error) {
return null;
}