- SUMMARY.md: userEvent v14 migration + vi.useFakeTimers() for ReviewStep - STATE.md: advanced to completed 05-03, added patterns as decisions - ROADMAP.md: phase 5 now 4/4 plans complete (Complete status) - REQUIREMENTS.md: TECH-05 marked complete
9.8 KiB
9.8 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 03-wizard-ui | 03 | execute | 3 |
|
|
true |
|
|
Purpose: Satisfies WIZD-01 (popularity-sorted card grid) and WIZD-04 (remote name validation). Output: src/components/wizard/BackendSelectionStep.tsx, BackendSelectionStep tests GREEN.
<execution_context> @C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/phases/03-wizard-ui/03-CONTEXT.md @.planning/phases/03-wizard-ui/03-RESEARCH.md @.planning/phases/03-wizard-ui/03-01-SUMMARY.md @.planning/phases/03-wizard-ui/03-02-SUMMARY.mdFrom src/store/types.ts:
export type BackendType = 'azureblob' | 's3' | 's3-compatible';
export type WizardAction =
| { type: 'SET_STEP'; payload: number }
| { type: 'SET_BACKEND_TYPE'; payload: BackendType }
| { type: 'SET_REMOTE_NAME'; payload: string }
| { type: 'SET_REMOTE_PARAMS'; payload: Record<string, string> }
| { type: 'SET_DEPLOYMENT'; payload: Partial<WizardState['deployment']> }
| { type: 'RESET' };
// INITIAL_STATE.remote.name = ''
// INITIAL_STATE.remote.backendType = null
From src/store/context.tsx:
export function useWizard(): { state: WizardState; dispatch: React.Dispatch<WizardAction> }
From src/components/ui/BackendCard.tsx (created in Plan 02):
export function BackendCard(props: {
name: string;
description: string;
selected?: boolean;
onClick: () => void;
}): JSX.Element
Remote name validation regex (from RESEARCH.md):
const remoteNameSchema = z.object({
name: z.string()
.min(1, 'Remote name is required')
.regex(/^[a-zA-Z0-9_-]+$/, 'Only letters, numbers, dashes, and underscores allowed'),
});
react-hook-form mode pattern (LOCKED — from RESEARCH.md):
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(remoteNameSchema),
mode: 'onSubmit', // no errors on initial fill
reValidateMode: 'onChange', // live errors once submitted once
defaultValues: { name: state.remote.name },
});
The component uses react-hook-form for the remote name field with `mode: 'onSubmit'` and `reValidateMode: 'onChange'`. The backend cards trigger `handleSubmit` internally — clicking a card submits the form, and if validation passes, it dispatches the three actions (SET_REMOTE_NAME → SET_BACKEND_TYPE → SET_REMOTE_PARAMS → SET_STEP).
Backend list (hardcoded display order — WIZD-01 popularity sort):
1. azureblob → "Azure Blob Storage" / "Microsoft Azure cloud storage"
2. s3 → "Amazon S3" / "AWS Simple Storage Service"
3. s3-compatible → "S3-Compatible" / "Wasabi, MinIO, Cloudflare R2, and others"
Implementation approach:
- `useForm` with zodResolver for name validation; `mode: 'onSubmit'`, `reValidateMode: 'onChange'`
- `defaultValues: { name: state.remote.name }` to restore previously entered name on back-nav
- Each BackendCard's `onClick` calls a handler that sets a `pendingBackend` ref, then calls `handleSubmit(onValidSubmit)()`
- `onValidSubmit` receives validated values, dispatches: `SET_REMOTE_NAME` (with validated name), `SET_BACKEND_TYPE` (with pendingBackend), `SET_REMOTE_PARAMS({})` (clear stale params), `SET_STEP(1)`
- CRITICAL dispatch order: SET_BACKEND_TYPE before SET_REMOTE_PARAMS({}) — SET_REMOTE_PARAMS clears old backend's params, SET_BACKEND_TYPE sets the new type. The reducer handles each action independently so order matters for clarity, not correctness.
- ANTI-PATTERN WARNING: Do NOT dispatch RESET — that wipes deployment options. Use SET_REMOTE_PARAMS({}) only.
Render structure:
```
<div> (outermost wrapper)
<h2>Step 1: Select Backend</h2>
<form onSubmit={handleSubmit(onValidSubmit)}>
Remote name section (at top — WIZD-04):
<label> + <input id="remote-name" ...register('name') />
{errors.name && <p role="alert">{errors.name.message}</p>}
Backend cards section (below name — WIZD-01):
<div role="list" or data-testid="backend-cards">
<BackendCard name="Azure Blob Storage" onClick={...} />
<BackendCard name="Amazon S3" onClick={...} />
<BackendCard name="S3-Compatible" onClick={...} />
</div>
</form>
</div>
```
Now update the test stub at `src/components/wizard/BackendSelectionStep.test.tsx` to make all tests GREEN. Tests must wrap the component in `WizardProvider` and use `@testing-library/react` render + screen + fireEvent. Import `WizardProvider` from `../../store/context`.
Test patterns:
- Render: `render(<WizardProvider><BackendSelectionStep /></WizardProvider>)`
- WIZD-01 card order: `const cards = screen.getAllByRole('button', { name: /Azure|Amazon|S3-Compatible/ })` — check order by textContent
- WIZD-01 click dispatches: Mock or spy on dispatch is complex with context. Instead verify navigation by checking that after clicking a card with a valid name pre-set (set state.remote.name via initial state), the appropriate DOM change happens. Alternatively, use a custom WizardProvider wrapper with observable dispatch for testing.
- WIZD-04 errors: `fireEvent.click(getByRole('button', { name: /Azure/i }))` with empty name → `await screen.findByRole('alert')` or `screen.getByText(/required/i)`
<success_criteria>
- BackendSelectionStep.tsx exists with named export
- All WIZD-01 tests green: three cards render, Azure first, click dispatches correct actions
- All WIZD-04 tests green: name field at top, no errors before submit, inline errors after failed submit, valid names pass
- Clicking a card with invalid name shows validation error and does NOT navigate to step 1
- Component uses
mode: 'onSubmit', reValidateMode: 'onChange'(locked UX decision) - Dispatches SET_REMOTE_PARAMS({}) on backend selection (clears stale params — critical for correctness)
- Does NOT dispatch RESET (deployment options must be preserved) </success_criteria>