Files
Ready2Blob/src/components/wizard/BackendSelectionStep.test.tsx
T
kawa 8b7daa860c test(11-02): add failing tests for auto-scroll to first error on validation failure
- Add scrollIntoView mock (vi.fn()) in beforeEach to both test files
- Add POLISH-04 test in BackendSelectionStep: scrolls to remote-name on empty submit
- Add POLISH-04 test in RemoteConfigStep: scrolls to first errored field on empty submit
2026-04-01 13:29:48 +02:00

162 lines
6.5 KiB
TypeScript

// @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(
<WizardProvider>
<BackendSelectionStep />
</WizardProvider>
);
}
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();
});
});
});
});