Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions frontend/src/__tests__/ReportIssue.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import ReportIssue from '../components/ReportIssue';

// Regression guard: the report widget used to ignore resp.ok and always show a
// success checkmark — so a failed POST (4xx/5xx) silently dropped the report.

describe('ReportIssue', () => {
beforeEach(() => {
global.fetch = vi.fn();
});
afterEach(() => {
vi.restoreAllMocks();
});

function openAndSend(text) {
fireEvent.click(screen.getByTitle('Report an issue'));
fireEvent.change(screen.getByPlaceholderText(/Describe what happened/i), {
target: { value: text },
});
fireEvent.click(screen.getByRole('button', { name: /Send Report/i }));
}

it('shows a failure message (NOT success) when the backend POST fails', async () => {
global.fetch.mockResolvedValue({ ok: false });
render(<ReportIssue />);
openAndSend('Inventory page is blank');

expect(await screen.findByText(/Couldn't send your report/i)).toBeInTheDocument();
expect(screen.queryByText(/We'll look into it/i)).not.toBeInTheDocument();
});

it('shows success when the report actually sends', async () => {
global.fetch.mockResolvedValue({ ok: true });
render(<ReportIssue />);
openAndSend('Just a note');

expect(await screen.findByText(/We'll look into it/i)).toBeInTheDocument();
});
});
30 changes: 24 additions & 6 deletions frontend/src/components/ReportIssue.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default function ReportIssue() {
const [description, setDescription] = useState('');
const [sent, setSent] = useState(false);
const [sending, setSending] = useState(false);
const [failed, setFailed] = useState(false);

const submit = async () => {
if (!description.trim()) return;
Expand All @@ -24,21 +25,27 @@ export default function ReportIssue() {
timestamp: new Date().toISOString(),
};

let ok = false;
try {
// Try to send to backend (which will log + optionally notify)
await fetch('/api/feedback', {
const resp = await fetch('/api/feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(report),
});
ok = resp.ok; // a 4xx/5xx is NOT a successful submission
} catch {
// Even if backend fails, log locally
console.log('Issue report:', report);
ok = false;
}

setSending(false);
setSent(true);
setTimeout(() => { setSent(false); setOpen(false); setDescription(''); }, 2000);
if (ok) {
setSent(true);
setTimeout(() => { setSent(false); setOpen(false); setDescription(''); }, 2000);
} else {
// Don't falsely confirm — let the customer retry or call instead.
console.warn('Issue report failed to send:', report);
setFailed(true);
}
};

if (!open) {
Expand Down Expand Up @@ -69,6 +76,17 @@ export default function ReportIssue() {
<CheckCircle size={20} />
<span className="font-medium">Thank you! We'll look into it.</span>
</div>
) : failed ? (
<div className="py-4 text-sm">
<p role="alert" className="text-red-600 font-medium mb-1">Couldn't send your report.</p>
<p className="text-gray-500 mb-3">Please try again, or call our showroom and we'll help right away.</p>
<button
onClick={() => setFailed(false)}
className="px-4 py-2 bg-red-500 text-white text-sm font-medium rounded-lg hover:bg-red-600 transition-colors"
>
Try again
</button>
</div>
) : (
<>
<textarea
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/pages/Appointments.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,12 @@ const TimeSlotPicker = ({ date, onSelect, onBack }) => {
if (data.error) {
setError(data.error);
} else {
setSlots(data);
// Normalize so a malformed 200 (missing/!array available_slots) can't
// crash the render at .length / .map.
setSlots({
...data,
available_slots: Array.isArray(data.available_slots) ? data.available_slots : [],
});
}
})
.catch(() => setError('Unable to load available times. Please try again.'))
Expand Down
1 change: 1 addition & 0 deletions frontend/src/pages/InventoryBrowse.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,7 @@ export default function InventoryBrowse({ adminAuthed = false, onAskTex, onCreat
const fetchInventory = useCallback(async () => {
try {
setLoading(true);
setError(null); // clear any stale error so "Try Again" can recover
const resp = await fetch('/api/marketing/inventory-context');
if (!resp.ok) throw new Error('Failed to load inventory');
const data = await resp.json();
Expand Down
Loading