// @vitest-environment jsdom // Covers WIZD-01: card grid renders Azure Blob, Amazon S3, S3-Compatible (Azure first) // Covers WIZD-04: remote name field validates alphanumeric/dash/underscore // Covers POLISH-04: auto-scroll to first errored field on validation failure import { describe, it, expect, beforeEach, vi } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { WizardProvider } from '../../store/context'; import { BackendSelectionStep } from './BackendSelectionStep'; function renderStep() { return render( ); } describe('BackendSelectionStep', () => { beforeEach(() => { Element.prototype.scrollIntoView = vi.fn(); }); describe('WIZD-01: backend card grid', () => { it('renders Azure Blob Storage card', () => { renderStep(); expect(screen.getByText('Azure Blob Storage')).toBeDefined(); }); it('renders Amazon S3 card', () => { renderStep(); expect(screen.getByText('Amazon S3')).toBeDefined(); }); it('renders S3-Compatible card', () => { renderStep(); expect(screen.getByText('S3-Compatible')).toBeDefined(); }); it('Azure Blob is listed before S3 in DOM order', () => { renderStep(); const buttons = screen.getAllByRole('button'); // Find Azure and S3 buttons by their accessible names const azureIdx = buttons.findIndex(b => b.textContent?.includes('Azure Blob Storage')); const s3Idx = buttons.findIndex(b => b.textContent?.includes('Amazon S3')); expect(azureIdx).toBeGreaterThanOrEqual(0); expect(s3Idx).toBeGreaterThanOrEqual(0); expect(azureIdx).toBeLessThan(s3Idx); }); it('clicking a backend card dispatches SET_BACKEND_TYPE and SET_STEP', async () => { const user = userEvent.setup(); renderStep(); // Fill in a valid remote name first const nameInput = screen.getByRole('textbox'); await user.clear(nameInput); await user.type(nameInput, 'my-remote'); // Click the Azure card — if navigation dispatches SET_STEP(1), component may unmount // We verify no validation error appears (meaning dispatch proceeded) const azureButton = screen.getAllByRole('button').find(b => b.textContent?.includes('Azure Blob Storage'))!; expect(azureButton).toBeDefined(); await user.click(azureButton); // After successful navigation step fires, no validation alert should be present expect(screen.queryByRole('alert')).toBeNull(); }); }); describe('RemoteNamePreview integration', () => { it('shows empty-state placeholder [my-remote] on initial render', () => { renderStep(); expect(screen.getByText('[my-remote]')).toBeDefined(); }); it('updates preview to [test-remote] as user types', async () => { const user = userEvent.setup(); renderStep(); const nameInput = screen.getByRole('textbox'); await user.clear(nameInput); await user.type(nameInput, 'test-remote'); expect(screen.getByText('[test-remote]')).toBeDefined(); }); }); describe('WIZD-04: remote name field', () => { it('renders remote name input at the top of the step', () => { renderStep(); const nameInput = screen.getByRole('textbox'); expect(nameInput).toBeDefined(); // Verify input appears before backend cards in DOM const container = nameInput.closest('form') ?? document.body; const allButtons = container.querySelectorAll('button'); const inputPosition = Array.from(container.querySelectorAll('input, button')).indexOf(nameInput as HTMLInputElement); const firstButtonPosition = Array.from(container.querySelectorAll('input, button')).indexOf(allButtons[0]); expect(inputPosition).toBeLessThan(firstButtonPosition); }); it('shows no error before first Next attempt', () => { renderStep(); // No alert role should be present on initial render expect(screen.queryByRole('alert')).toBeNull(); }); it('shows inline error after first Next attempt with invalid name', async () => { const user = userEvent.setup(); renderStep(); // Click a card without filling in name — triggers validation const azureButton = screen.getAllByRole('button').find(b => b.textContent?.includes('Azure Blob Storage'))!; await user.click(azureButton); // Error should appear after async validation await waitFor(() => { const alert = screen.getByRole('alert'); expect(alert).toBeDefined(); expect(alert.textContent).toMatch(/required/i); }); }); it('accepts alphanumeric, dashes, and underscores', async () => { const user = userEvent.setup(); renderStep(); const nameInput = screen.getByRole('textbox'); await user.clear(nameInput); await user.type(nameInput, 'my-remote_01'); const azureButton = screen.getAllByRole('button').find(b => b.textContent?.includes('Azure Blob Storage'))!; await user.click(azureButton); // Valid name — no alert should appear (wait briefly to confirm no error appears) await waitFor(() => { expect(screen.queryByRole('alert')).toBeNull(); }); }); it('rejects names with spaces or special characters', async () => { const user = userEvent.setup(); renderStep(); const nameInput = screen.getByRole('textbox'); await user.clear(nameInput); await user.type(nameInput, 'my remote!'); const azureButton = screen.getAllByRole('button').find(b => b.textContent?.includes('Azure Blob Storage'))!; await user.click(azureButton); // Invalid name — alert should appear after async validation await waitFor(() => { expect(screen.getByRole('alert')).toBeDefined(); expect(screen.getByText(/letters, numbers, dashes/i)).toBeDefined(); }); }); }); describe('POLISH-04: auto-scroll to first error', () => { it('scrolls to remote-name field when submitted with empty name', async () => { const user = userEvent.setup(); renderStep(); const nameInput = screen.getByRole('textbox'); await user.clear(nameInput); // Submit via Next button with empty name to trigger validation failure const nextButton = screen.getByRole('button', { name: /next/i }); await user.click(nextButton); await waitFor(() => { expect(Element.prototype.scrollIntoView).toHaveBeenCalled(); }); }); }); });