diff --git a/frontend/src/__tests__/NotFound.test.jsx b/frontend/src/__tests__/NotFound.test.jsx new file mode 100644 index 000000000..9d9cce37b --- /dev/null +++ b/frontend/src/__tests__/NotFound.test.jsx @@ -0,0 +1,39 @@ +import { render } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import NotFound from '../pages/NotFound'; + +const setSearchParams = vi.fn(); + +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useLocation: () => ({ pathname: '/missing', search: '?q=abc' }), + useNavigate: () => vi.fn(), + useSearchParams: () => [new URLSearchParams(), setSearchParams], + }; +}); + +vi.mock('../components/SearchInput', () => ({ + default: ({ value, onChange }) => onChange(event.target.value)} />, +})); + +vi.mock('../components/ReportBugModal', () => ({ + default: () => null, +})); + +describe('NotFound page', () => { + beforeEach(() => { + setSearchParams.mockClear(); + }); + + test('syncs the search query without adding a history entry', () => { + render( + + + + ); + + expect(setSearchParams).toHaveBeenCalledWith({ q: '/missing?q=abc' }, { replace: true }); + }); +}); diff --git a/frontend/src/__tests__/ReportBugModal.test.jsx b/frontend/src/__tests__/ReportBugModal.test.jsx new file mode 100644 index 000000000..493a78e25 --- /dev/null +++ b/frontend/src/__tests__/ReportBugModal.test.jsx @@ -0,0 +1,17 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import ReportBugModal from '../components/ReportBugModal' + +describe('ReportBugModal', () => { + test('exposes dialog semantics and closes on Escape', () => { + const onClose = vi.fn() + + render() + + const dialog = screen.getByRole('dialog', { name: /report broken link/i }) + expect(dialog).toHaveAttribute('aria-modal', 'true') + expect(dialog).toHaveFocus() + + fireEvent.keyDown(document, { key: 'Escape' }) + expect(onClose).toHaveBeenCalledTimes(1) + }) +}) diff --git a/frontend/src/components/ReportBugModal.jsx b/frontend/src/components/ReportBugModal.jsx index e3e36b943..3ff00da5d 100644 --- a/frontend/src/components/ReportBugModal.jsx +++ b/frontend/src/components/ReportBugModal.jsx @@ -1,124 +1,135 @@ -import React, { useState } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { X, Bug, Loader2 } from 'lucide-react'; -import toast from 'react-hot-toast'; -import { bugsApi } from '../services/api'; +import { useEffect, useRef, useState } from 'react' +import { motion } from 'framer-motion' +import { X, Bug } from 'lucide-react' +import toast from 'react-hot-toast' -export default function ReportBugModal({ isOpen, onClose }) { - const [title, setTitle] = useState(''); - const [description, setDescription] = useState(''); - const [isSubmitting, setIsSubmitting] = useState(false); +export default function ReportBugModal({ isOpen, onClose, invalidPath }) { + const [reportBody, setReportBody] = useState('') + const dialogRef = useRef(null) - const handleSubmit = async (e) => { - e.preventDefault(); - - if (!title.trim() || !description.trim()) { - toast.error('Please fill out both title and description.'); - return; + useEffect(() => { + if (!isOpen) return + + setReportBody( + `Invalid URL: ${invalidPath}\n\nPlease describe what you were expecting to find and any additional details that would help us fix this broken link.` + ) + + const dialog = dialogRef.current + if (dialog) { + dialog.focus() } + }, [isOpen, invalidPath]) + + useEffect(() => { + if (!isOpen) return + + const handleKeyDown = (event) => { + if (event.key === 'Escape') { + event.preventDefault() + onClose() + } + } + + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, [isOpen, onClose]) + + const handleSubmit = async (event) => { + event.preventDefault() - setIsSubmitting(true); try { - await bugsApi.submitBug(title, description); - toast.success('Bug reported successfully! Thank you.'); - setTitle(''); - setDescription(''); - onClose(); + await navigator.clipboard.writeText(reportBody) + toast.success('Bug report copied to clipboard!') + onClose() } catch (error) { - console.error('Error reporting bug:', error); - toast.error(error.message || 'Failed to report bug. Please try again later.'); - } finally { - setIsSubmitting(false); + toast.error('Unable to copy report to clipboard. Please try again.') } - }; + } + + if (!isOpen) { + return null + } return ( - - {isOpen && ( -
- + + + +
+
+
+ +
+
+

Report broken link

+

The invalid URL is prefilled so your report is ready to share.

+
+
+ + + +
+ +
+
+ +
+ {invalidPath}
+
- -
- - setTitle(e.target.value)} - placeholder="E.g., Resume builder fails to save" - className="w-full px-4 py-2 bg-background border border-border rounded-xl text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50" - maxLength={100} - required - /> -
- -
- -