diff --git a/frontend/src/components/ImportSitesModal.test.tsx b/frontend/src/components/ImportSitesModal.test.tsx
new file mode 100644
index 00000000..a9fc0465
--- /dev/null
+++ b/frontend/src/components/ImportSitesModal.test.tsx
@@ -0,0 +1,84 @@
+import { render, screen, fireEvent, waitFor } from '@testing-library/react'
+import ImportSitesModal from './ImportSitesModal'
+import { vi } from 'vitest'
+
+// Mock the upload API used by the component
+const mockUpload = vi.fn()
+vi.mock('../api/import', () => ({
+ uploadCaddyfilesMulti: (...args: unknown[]) => mockUpload(...(args as any[])),
+}))
+
+describe('ImportSitesModal', () => {
+ beforeEach(() => {
+ mockUpload.mockReset()
+ })
+
+ test('renders modal, add and remove sites, and edits textarea', () => {
+ const onClose = vi.fn()
+ render()
+
+ // modal container is present
+ expect(screen.getByTestId('multi-site-modal')).toBeInTheDocument()
+
+ // initially one textarea
+ const areas = screen.getAllByRole('textbox')
+ expect(areas.length).toBeGreaterThanOrEqual(1)
+
+ // add a site -> two textareas
+ fireEvent.click(screen.getByText('+ Add site'))
+ expect(screen.getAllByRole('textbox').length).toBe(areas.length + 1)
+
+ // remove the second site
+ const removeBtn = screen.getByText('Remove')
+ fireEvent.click(removeBtn)
+ expect(screen.getAllByRole('textbox').length).toBe(areas.length)
+
+ // type into textarea
+ const ta = screen.getAllByRole('textbox')[0]
+ fireEvent.change(ta, { target: { value: 'example.com { reverse_proxy 127.0.0.1:8080 }' } })
+ expect((ta as HTMLTextAreaElement).value).toContain('example.com')
+ })
+
+ test('reads multiple files via hidden input and submits successfully', async () => {
+ const onClose = vi.fn()
+ const onUploaded = vi.fn()
+ mockUpload.mockResolvedValueOnce(undefined)
+
+ const { container } = render()
+
+ // find the hidden file input
+ const input: HTMLInputElement | null = container.querySelector('input[type="file"]')
+ expect(input).toBeTruthy()
+
+ // create two files
+ const f1 = new File(['site1'], 'site1.caddy', { type: 'text/plain' })
+ const f2 = new File(['site2'], 'site2.caddy', { type: 'text/plain' })
+
+ // fire change event with files
+ fireEvent.change(input!, { target: { files: [f1, f2] } })
+
+ // after input, two textareas should appear
+ await waitFor(() => expect(screen.getAllByRole('textbox').length).toBe(2))
+
+ // submit
+ fireEvent.click(screen.getByText('Parse and Review'))
+
+ await waitFor(() => expect(mockUpload).toHaveBeenCalled())
+ expect(mockUpload).toHaveBeenCalledWith(['site1', 'site2'])
+ expect(onUploaded).toHaveBeenCalled()
+ expect(onClose).toHaveBeenCalled()
+ })
+
+ test('displays error when upload fails', async () => {
+ const onClose = vi.fn()
+ mockUpload.mockRejectedValueOnce(new Error('upload-failed'))
+
+ render()
+
+ // click submit with default empty site
+ fireEvent.click(screen.getByText('Parse and Review'))
+
+ // error message appears
+ await waitFor(() => expect(screen.getByText(/upload-failed|Upload failed/i)).toBeInTheDocument())
+ })
+})
diff --git a/frontend/src/components/ImportSitesModal.tsx b/frontend/src/components/ImportSitesModal.tsx
index 17e84a2d..e57d5cec 100644
--- a/frontend/src/components/ImportSitesModal.tsx
+++ b/frontend/src/components/ImportSitesModal.tsx
@@ -29,10 +29,10 @@ export default function ImportSitesModal({ visible, onClose, onUploaded }: Props
try {
const text = await files[i].text()
newSites.push(text)
- } catch (err) {
- // ignore read errors for individual files
- newSites.push('')
- }
+ } catch (_err) {
+ // ignore read errors for individual files
+ newSites.push('')
+ }
}
if (newSites.length > 0) setSites(newSites)
}