From 94a73f8c317e3853fdf171c7c2b7a35bc5fe4f4f Mon Sep 17 00:00:00 2001 From: Deepesh Kafaltiya Date: Mon, 1 Jun 2026 12:30:58 +0530 Subject: [PATCH 1/5] feat: implement reusable CopyToClipboard component for raw scan outputs --- frontend/src/components/CopyToClipboard.jsx | 47 +++++++++++++++++++++ frontend/src/pages/TaskDetails.tsx | 8 +--- 2 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 frontend/src/components/CopyToClipboard.jsx diff --git a/frontend/src/components/CopyToClipboard.jsx b/frontend/src/components/CopyToClipboard.jsx new file mode 100644 index 00000000..b2a7b54a --- /dev/null +++ b/frontend/src/components/CopyToClipboard.jsx @@ -0,0 +1,47 @@ +import React, { useState } from 'react'; + +const CopyToClipboard = ({ textToCopy }) => { + const [isCopied, setIsCopied] = useState(false); + + const handleCopy = async () => { + if (!textToCopy) return; + try { + await navigator.clipboard.writeText(textToCopy); + setIsCopied(true); + setTimeout(() => setIsCopied(false), 2000); + } catch (err) { + console.error('Failed to copy text: ', err); + } + }; + + return ( + + ); +}; + +export default CopyToClipboard; \ No newline at end of file diff --git a/frontend/src/pages/TaskDetails.tsx b/frontend/src/pages/TaskDetails.tsx index 2389983a..74d40c11 100644 --- a/frontend/src/pages/TaskDetails.tsx +++ b/frontend/src/pages/TaskDetails.tsx @@ -1,3 +1,4 @@ +import CopyToClipboard from '../components/CopyToClipboard'; import React, { useState, useEffect, useRef } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { motion, AnimatePresence } from 'framer-motion' @@ -1179,12 +1180,7 @@ export default function TaskDetails() { > {wrapRawOutput ? 'Disable Wrap' : 'Enable Wrap'} - + From 0884bade757b1ecd9d42afe38d00e1c50a028926 Mon Sep 17 00:00:00 2001 From: Deepesh Kafaltiya Date: Mon, 1 Jun 2026 12:43:49 +0530 Subject: [PATCH 2/5] feat: implement reusable CopyToClipboard component for raw scan outputs --- .../{CopyToClipboard.jsx => CopyToClipboard.tsx} | 12 +++++++++--- frontend/src/pages/TaskDetails.tsx | 10 ---------- 2 files changed, 9 insertions(+), 13 deletions(-) rename frontend/src/components/{CopyToClipboard.jsx => CopyToClipboard.tsx} (81%) diff --git a/frontend/src/components/CopyToClipboard.jsx b/frontend/src/components/CopyToClipboard.tsx similarity index 81% rename from frontend/src/components/CopyToClipboard.jsx rename to frontend/src/components/CopyToClipboard.tsx index b2a7b54a..9a5da452 100644 --- a/frontend/src/components/CopyToClipboard.jsx +++ b/frontend/src/components/CopyToClipboard.tsx @@ -1,9 +1,14 @@ import React, { useState } from 'react'; -const CopyToClipboard = ({ textToCopy }) => { - const [isCopied, setIsCopied] = useState(false); +// Defining an explicit type interface for the component props +interface CopyToClipboardProps { + textToCopy: string; +} - const handleCopy = async () => { +const CopyToClipboard: React.FC = ({ textToCopy }) => { + const [isCopied, setIsCopied] = useState(false); + + const handleCopy = async (): Promise => { if (!textToCopy) return; try { await navigator.clipboard.writeText(textToCopy); @@ -17,6 +22,7 @@ const CopyToClipboard = ({ textToCopy }) => { return ( ); }; -export default CopyToClipboard; \ No newline at end of file +export default CopyToClipboard; diff --git a/frontend/testing/unit/components/CopyToClipboard.test.tsx b/frontend/testing/unit/components/CopyToClipboard.test.tsx new file mode 100644 index 00000000..ace913c8 --- /dev/null +++ b/frontend/testing/unit/components/CopyToClipboard.test.tsx @@ -0,0 +1,62 @@ +import { act, fireEvent, render, screen } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import CopyToClipboard from '../../../src/components/CopyToClipboard'; + +describe('CopyToClipboard', () => { + const originalClipboard = navigator.clipboard; + const writeText = vi.fn(); + + beforeEach(() => { + vi.useFakeTimers(); + writeText.mockReset(); + Object.defineProperty(navigator, 'clipboard', { + configurable: true, + value: { writeText }, + }); + }); + + afterEach(() => { + vi.useRealTimers(); + Object.defineProperty(navigator, 'clipboard', { + configurable: true, + value: originalClipboard, + }); + }); + + it('copies text and returns to idle state', async () => { + writeText.mockResolvedValue(undefined); + + render(); + + fireEvent.click(screen.getByRole('button', { name: /copy output/i })); + await act(async () => {}); + + expect(writeText).toHaveBeenCalledWith('raw output'); + expect(screen.getByRole('button', { name: /copied!/i })).toBeInTheDocument(); + + act(() => { + vi.advanceTimersByTime(2000); + }); + + expect(screen.getByRole('button', { name: /copy output/i })).toBeInTheDocument(); + }); + + it('shows failure state when clipboard write fails', async () => { + writeText.mockRejectedValue(new Error('clipboard denied')); + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}); + + render(); + + fireEvent.click(screen.getByRole('button', { name: /copy output/i })); + await act(async () => {}); + + expect(screen.getByRole('button', { name: /copy failed/i })).toBeInTheDocument(); + + act(() => { + vi.advanceTimersByTime(2000); + }); + + expect(screen.getByRole('button', { name: /copy output/i })).toBeInTheDocument(); + consoleError.mockRestore(); + }); +});