docs(05-03): complete act()-warning elimination plan

- 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
This commit is contained in:
2026-03-30 11:44:59 +02:00
parent 913cbe836e
commit 53bbd00533
10 changed files with 1750 additions and 12 deletions
+4 -4
View File
@@ -10,8 +10,8 @@
- [x] **TECH-01**: User sees only the script output blocks matching their selected deployment targets (Intune and/or RMM) in ReviewStep
- [x] **TECH-02**: User can navigate back from ReviewStep using an explicit Back button
- [x] **TECH-03**: Backend list in BackendSelectionStep is automatically derived from BACKEND_REGISTRY keys (no hardcoded list)
- [ ] **TECH-04**: Dead `BackendFormValues<T>` export is removed from `src/schemas/index.ts`
- [ ] **TECH-05**: BackendSelectionStep test suite runs without `act()` warnings
- [x] **TECH-04**: Dead `BackendFormValues<T>` export is removed from `src/schemas/index.ts`
- [x] **TECH-05**: BackendSelectionStep test suite runs without `act()` warnings
### Backends
@@ -49,8 +49,8 @@
| TECH-01 | Phase 5 | Complete |
| TECH-02 | Phase 5 | Complete |
| TECH-03 | Phase 5 | Complete |
| TECH-04 | Phase 5 | Pending |
| TECH-05 | Phase 5 | Pending |
| TECH-04 | Phase 5 | Complete |
| TECH-05 | Phase 5 | Complete |
| BACK-01 | Phase 6 | Pending |
| BACK-02 | Phase 6 | Pending |
| BACK-03 | Phase 6 | Pending |
+2 -2
View File
@@ -23,7 +23,7 @@ Full phase details: [.planning/milestones/v1.0-ROADMAP.md](milestones/v1.0-ROADM
**Milestone Goal:** Ship all deferred tech debt and backlog features from v1.0 — scriptTargets filtering, UX consistency fixes, registry-driven backend list, 4 new backends, field validation, and contextual tooltips.
- [ ] **Phase 5: Tech Debt** - Fix all deferred v1.0 issues (scriptTargets filtering, Back button, registry-driven UI, dead export, test warnings)
- [x] **Phase 5: Tech Debt** - Fix all deferred v1.0 issues (scriptTargets filtering, Back button, registry-driven UI, dead export, test warnings) (completed 2026-03-30)
- [ ] **Phase 6: New Backends** - Add OneDrive, SFTP, Google Cloud Storage, and Backblaze B2 to the backend registry and wizard
- [ ] **Phase 7: Validation & UX Polish** - Add field-level format validation and contextual tooltips across all backends
@@ -78,6 +78,6 @@ Plans:
| 2. Generators | v1.0 | 4/4 | Complete | 2026-03-26 |
| 3. Wizard UI | v1.0 | 5/5 | Complete | 2026-03-27 |
| 4. Review, Download & Security | v1.0 | 5/5 | Complete | 2026-03-27 |
| 5. Tech Debt | 3/4 | In Progress| | - |
| 5. Tech Debt | 4/4 | Complete | 2026-03-30 | - |
| 6. New Backends | v1.1 | 0/TBD | Not started | - |
| 7. Validation & UX Polish | v1.1 | 0/TBD | Not started | - |
+9 -6
View File
@@ -3,14 +3,14 @@ gsd_state_version: 1.0
milestone: v1.1
milestone_name: Backlog & Tech Debt
status: planning
stopped_at: Completed 05-tech-debt 05-01-PLAN.md
last_updated: "2026-03-30T07:39:32.011Z"
stopped_at: Completed 05-tech-debt 05-03-PLAN.md
last_updated: "2026-03-30T07:44:48.651Z"
last_activity: 2026-03-27 — v1.1 roadmap created, 11 requirements mapped across 3 phases
progress:
total_phases: 3
completed_phases: 0
completed_phases: 1
total_plans: 4
completed_plans: 3
completed_plans: 4
percent: 0
---
@@ -51,6 +51,7 @@ Progress: [░░░░░░░░░░] 0% (v1.1)
| Phase 05-tech-debt P00 | 3 | 2 tasks | 2 files |
| Phase 05-tech-debt P02 | 4min | 1 tasks | 1 files |
| Phase 05-tech-debt P01 | 5 | 2 tasks | 4 files |
| Phase 05-tech-debt P03 | 3 | 2 tasks | 2 files |
## Accumulated Context
@@ -68,6 +69,8 @@ Recent decisions affecting v1.1 work:
- [Phase 05-tech-debt]: Unmount OutputBlocks entirely (conditional render with &&) not CSS hidden — avoids stale component state when toggling scriptTargets
- [Phase 05-tech-debt]: Keep useMemo calls for all scripts even when OutputBlocks hidden — ZIP handler always needs the content for dynamic bundle building
- [Phase 05-tech-debt]: BACKEND_REGISTRY shape enriched to { displayName, description, fields } — registry is single source of truth for display AND field data; BackendSelectionStep wired via Object.entries(BACKEND_REGISTRY)
- [Phase 05-tech-debt]: userEvent.setup() per test body pattern established — replaces fireEvent for async React state interactions
- [Phase 05-tech-debt]: vi.useFakeTimers() in beforeEach + vi.useRealTimers() in afterEach pattern established for suppressing setTimeout act() warnings
### Pending Todos
@@ -82,6 +85,6 @@ None yet.
## Session Continuity
Last session: 2026-03-30T07:39:32.005Z
Stopped at: Completed 05-tech-debt 05-01-PLAN.md
Last session: 2026-03-30T07:44:48.644Z
Stopped at: Completed 05-tech-debt 05-03-PLAN.md
Resume file: None
+333
View File
@@ -0,0 +1,333 @@
---
phase: 03-wizard-ui
plan: "01"
type: execute
wave: 1
depends_on: []
files_modified:
- vite.config.ts
- src/App.test.tsx
- src/components/wizard/BackendSelectionStep.test.tsx
- src/components/wizard/RemoteConfigStep.test.tsx
- src/components/wizard/StepIndicator.test.tsx
autonomous: true
requirements:
- WIZD-01
- WIZD-02
- WIZD-03
- WIZD-04
- BACK-01
- BACK-02
- BACK-03
must_haves:
truths:
- "Vitest runs React component tests without 'document is not defined' errors"
- "All four test stub files exist and fail RED with descriptive 'not yet implemented' messages"
- "npx vitest run exits non-zero (tests fail) confirming stubs are RED"
artifacts:
- path: "vite.config.ts"
provides: "jsdom test environment config"
contains: "test: { environment: 'jsdom'"
- path: "src/App.test.tsx"
provides: "Wave 0 stub for WIZD-02 step routing"
- path: "src/components/wizard/BackendSelectionStep.test.tsx"
provides: "Wave 0 stubs for WIZD-01, WIZD-04"
- path: "src/components/wizard/RemoteConfigStep.test.tsx"
provides: "Wave 0 stubs for BACK-01, BACK-02, BACK-03"
- path: "src/components/wizard/StepIndicator.test.tsx"
provides: "Wave 0 stubs for WIZD-03"
key_links:
- from: "vite.config.ts"
to: "src/**/*.test.tsx"
via: "test.environment: 'jsdom'"
pattern: "environment.*jsdom"
---
<objective>
Set up the Wave 0 test infrastructure: configure Vitest for jsdom component testing and create failing test stubs for all Phase 3 wizard components.
Purpose: Establish the RED baseline before implementation — tests must fail in a descriptive way so Plans 0305 can drive to GREEN.
Output: Updated vite.config.ts and four test stub files.
</objective>
<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>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/03-wizard-ui/03-CONTEXT.md
@.planning/phases/03-wizard-ui/03-RESEARCH.md
@.planning/phases/03-wizard-ui/03-VALIDATION.md
<interfaces>
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
From src/store/types.ts:
```typescript
export interface WizardState {
currentStep: number;
remote: {
name: string;
backendType: BackendType | null;
params: Record<string, string>;
};
deployment: {
includeInstall: boolean;
configPath: 'machine-wide' | 'user-profile';
scriptTargets: ('intune' | 'rmm')[];
};
}
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' };
```
From src/schemas/registry.ts:
```typescript
export type BackendType = 'azureblob' | 's3' | 's3-compatible';
```
From src/store/context.tsx:
```typescript
export function useWizard(): WizardContextValue;
export function WizardProvider({ children }: { children: React.ReactNode }): JSX.Element;
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Configure Vitest jsdom environment in vite.config.ts</name>
<files>vite.config.ts</files>
<action>
Update vite.config.ts to add a `test` block inside `defineConfig`. The existing config has only `plugins: [react(), tailwindcss()]`. Add:
```typescript
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [react(), tailwindcss()],
test: {
environment: 'jsdom',
passWithNoTests: true,
},
});
```
Do NOT add `/// <reference types="vitest" />` — this is not needed with vitest ^4.x when using `test` config inside `defineConfig`. The `passWithNoTests: true` is already established project convention (see STATE.md decision from Phase 01-01).
</action>
<verify>
<automated>npx vitest run --reporter=verbose 2>&1 | head -20</automated>
</verify>
<done>Vitest starts without "document is not defined" errors. If no test files exist yet the suite exits 0 due to passWithNoTests: true.</done>
</task>
<task type="auto">
<name>Task 2: Create Wave 0 test stubs (RED baseline for all 7 requirements)</name>
<files>
src/App.test.tsx,
src/components/wizard/BackendSelectionStep.test.tsx,
src/components/wizard/RemoteConfigStep.test.tsx,
src/components/wizard/StepIndicator.test.tsx
</files>
<action>
Create the `src/components/wizard/` directory (it does not exist yet). Create four test stub files. Each stub imports the component it will test and throws a todo error — this gives RED tests with clear names rather than import errors.
NOTE: The component files do NOT exist yet. Import them anyway. Vitest will fail at import with "Cannot find module" — this is the correct RED state.
**src/App.test.tsx**
```typescript
// @vitest-environment jsdom
// Covers WIZD-02: App renders the correct step component for currentStep 0, 1, 2
import { describe, it, expect } from 'vitest';
describe('App — step routing', () => {
it('renders BackendSelectionStep when currentStep is 0', () => {
expect.fail('not yet implemented');
});
it('renders RemoteConfigStep when currentStep is 1', () => {
expect.fail('not yet implemented');
});
it('renders DeploymentStep when currentStep is 2', () => {
expect.fail('not yet implemented');
});
});
```
**src/components/wizard/BackendSelectionStep.test.tsx**
```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
import { describe, it, expect } from 'vitest';
describe('BackendSelectionStep', () => {
describe('WIZD-01: backend card grid', () => {
it('renders Azure Blob Storage card', () => {
expect.fail('not yet implemented');
});
it('renders Amazon S3 card', () => {
expect.fail('not yet implemented');
});
it('renders S3-Compatible card', () => {
expect.fail('not yet implemented');
});
it('Azure Blob is listed before S3 in DOM order', () => {
expect.fail('not yet implemented');
});
it('clicking a backend card dispatches SET_BACKEND_TYPE and SET_STEP', () => {
expect.fail('not yet implemented');
});
});
describe('WIZD-04: remote name field', () => {
it('renders remote name input at the top of the step', () => {
expect.fail('not yet implemented');
});
it('shows no error before first Next attempt', () => {
expect.fail('not yet implemented');
});
it('shows inline error after first Next attempt with invalid name', () => {
expect.fail('not yet implemented');
});
it('accepts alphanumeric, dashes, and underscores', () => {
expect.fail('not yet implemented');
});
it('rejects names with spaces or special characters', () => {
expect.fail('not yet implemented');
});
});
});
```
**src/components/wizard/RemoteConfigStep.test.tsx**
```typescript
// @vitest-environment jsdom
// Covers BACK-01: Azure Blob config form — account + SAS/Key toggle, both values preserved
// Covers BACK-02: Amazon S3 config form — access_key_id, secret_access_key, region
// Covers BACK-03: S3-Compatible config form — same as S3 plus endpoint field
import { describe, it, expect } from 'vitest';
describe('RemoteConfigStep', () => {
describe('BACK-01: Azure Blob form', () => {
it('renders Storage Account Name field', () => {
expect.fail('not yet implemented');
});
it('shows SAS URL field by default (default auth method)', () => {
expect.fail('not yet implemented');
});
it('switching auth toggle to Access Key shows key field and hides SAS URL', () => {
expect.fail('not yet implemented');
});
it('switching auth toggle does not clear the hidden field value', () => {
expect.fail('not yet implemented');
});
});
describe('BACK-02: Amazon S3 form', () => {
it('renders access_key_id field', () => {
expect.fail('not yet implemented');
});
it('renders secret_access_key field', () => {
expect.fail('not yet implemented');
});
it('renders region field', () => {
expect.fail('not yet implemented');
});
});
describe('BACK-03: S3-Compatible form', () => {
it('renders endpoint field in addition to S3 fields', () => {
expect.fail('not yet implemented');
});
});
});
```
**src/components/wizard/StepIndicator.test.tsx**
```typescript
// @vitest-environment jsdom
// Covers WIZD-03: going back preserves remote.params; deployment options are untouched
import { describe, it, expect } from 'vitest';
describe('StepIndicator', () => {
describe('WIZD-03: back navigation preserves state', () => {
it('clicking a completed step dispatches SET_STEP', () => {
expect.fail('not yet implemented');
});
it('clicking back to step 0 dispatches SET_REMOTE_PARAMS({}) to clear params', () => {
expect.fail('not yet implemented');
});
it('clicking back to step 0 does NOT dispatch RESET (deployment preserved)', () => {
expect.fail('not yet implemented');
});
it('step 0 shows as active when currentStep is 0', () => {
expect.fail('not yet implemented');
});
it('completed steps are clickable', () => {
expect.fail('not yet implemented');
});
});
});
```
</action>
<verify>
<automated>npx vitest run --reporter=verbose 2>&1 | tail -30</automated>
</verify>
<done>
All four test files exist. Vitest runs and reports failures (RED). The failures are either "Cannot find module" (acceptable — component files not yet created) or "not yet implemented" (from expect.fail). No passing tests exist yet. vite.config.ts has jsdom environment.
</done>
</task>
</tasks>
<verification>
Run after both tasks complete:
```bash
npx vitest run --reporter=verbose 2>&1
```
Expected: Vitest runs (no "document is not defined"), tests fail RED with module-not-found or "not yet implemented" errors, suite reports failures. The key outcome is that jsdom works and tests are scaffolded.
</verification>
<success_criteria>
- vite.config.ts has `test: { environment: 'jsdom', passWithNoTests: true }`
- Four test files exist under src/ with descriptive failing test names
- `npx vitest run` runs without crashing the vitest process itself (failures are expected)
- Test names directly trace to requirements (WIZD-01, WIZD-02, WIZD-03, WIZD-04, BACK-01, BACK-02, BACK-03)
</success_criteria>
<output>
After completion, create `.planning/phases/03-wizard-ui/03-01-SUMMARY.md`
</output>
+426
View File
@@ -0,0 +1,426 @@
---
phase: 03-wizard-ui
plan: "02"
type: execute
wave: 2
depends_on:
- "03-01"
files_modified:
- src/components/ui/BackendCard.tsx
- src/components/ui/PasswordField.tsx
- src/components/ui/FieldRenderer.tsx
- src/components/wizard/AzureAuthToggle.tsx
autonomous: true
requirements:
- BACK-01
- BACK-02
- BACK-03
must_haves:
truths:
- "BackendCard renders a clickable card with backend name and calls onClick when clicked"
- "PasswordField renders a password input with a show/hide eye toggle button"
- "FieldRenderer renders the correct input type for text, password, select, and toggle FieldDef entries"
- "AzureAuthToggle renders a segmented SAS/Key toggle, shows the active field, and defaults to SAS"
artifacts:
- path: "src/components/ui/BackendCard.tsx"
provides: "Clickable backend selection card"
exports: ["BackendCard"]
- path: "src/components/ui/PasswordField.tsx"
provides: "Password input with show/hide toggle"
exports: ["PasswordField"]
- path: "src/components/ui/FieldRenderer.tsx"
provides: "Single FieldDef renderer for registry-driven forms"
exports: ["FieldRenderer"]
- path: "src/components/wizard/AzureAuthToggle.tsx"
provides: "Azure SAS vs Access Key segmented toggle"
exports: ["AzureAuthToggle"]
key_links:
- from: "src/components/wizard/AzureAuthToggle.tsx"
to: "src/schemas/registry.ts"
via: "renders key and sas_url FieldDef entries via PasswordField"
pattern: "PasswordField"
- from: "src/components/ui/FieldRenderer.tsx"
to: "src/schemas/registry.ts"
via: "accepts FieldDef and renders the correct input element"
pattern: "FieldDef"
---
<objective>
Create the four reusable UI atom components that the wizard step components depend on: BackendCard, PasswordField, FieldRenderer, and AzureAuthToggle.
Purpose: Establish shared building blocks so Plans 03 and 04 can implement their step components without duplicating input logic.
Output: Four new component files in src/components/ui/ and src/components/wizard/.
</objective>
<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>
<context>
@.planning/PROJECT.md
@.planning/phases/03-wizard-ui/03-CONTEXT.md
@.planning/phases/03-wizard-ui/03-RESEARCH.md
<interfaces>
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
From src/schemas/registry.ts:
```typescript
export type BackendType = 'azureblob' | 's3' | 's3-compatible';
export interface FieldDef {
key: string; // MUST match rclone config key exactly (snake_case)
label: string;
inputType: 'text' | 'password' | 'select' | 'toggle';
required: boolean;
placeholder?: string;
helpText?: string;
options?: { value: string; label: string }[]; // for inputType: 'select'
}
export const BACKEND_REGISTRY: Record<BackendType, FieldDef[]> = {
azureblob: [
{ key: 'account', label: 'Storage Account Name', inputType: 'text', required: true },
{ key: 'key', label: 'Access Key', inputType: 'password', required: false },
{ key: 'sas_url', label: 'SAS URL', inputType: 'password', required: false },
],
s3: [
{ key: 'provider', label: 'Provider', inputType: 'select', required: true },
{ key: 'access_key_id', label: 'Access Key ID', inputType: 'text', required: true },
{ key: 'secret_access_key', label: 'Secret Access Key',inputType: 'password', required: true },
{ key: 'region', label: 'Region', inputType: 'text', required: true },
],
's3-compatible': [
{ key: 'provider', label: 'Provider', inputType: 'select', required: true },
{ key: 'access_key_id', label: 'Access Key ID', inputType: 'text', required: true },
{ key: 'secret_access_key', label: 'Secret Access Key',inputType: 'password', required: true },
{ key: 'endpoint', label: 'Endpoint URL', inputType: 'text', required: true },
{ key: 'region', label: 'Region', inputType: 'text', required: false },
],
};
```
From react-hook-form (installed as react-hook-form ^7.72.0):
```typescript
// UseFormRegister and FieldError types used in FieldRenderer and PasswordField
import type { UseFormRegister, FieldError } from 'react-hook-form';
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Create BackendCard and PasswordField UI atoms</name>
<files>src/components/ui/BackendCard.tsx, src/components/ui/PasswordField.tsx</files>
<action>
Create `src/components/ui/` directory and the two simplest atom components.
**src/components/ui/BackendCard.tsx**
A clickable card for the backend selection grid. Props: `name` (display string), `description` (short subtitle), `onClick` (void callback), `selected` (boolean for visual highlight).
```tsx
import type { ButtonHTMLAttributes } from 'react';
interface BackendCardProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'> {
name: string;
description: string;
selected?: boolean;
onClick: () => void;
}
export function BackendCard({ name, description, selected = false, onClick, ...rest }: BackendCardProps) {
return (
<button
type="button"
onClick={onClick}
data-selected={selected}
className={[
'flex flex-col items-start gap-1 rounded-lg border-2 p-4 text-left transition-colors',
selected
? 'border-blue-600 bg-blue-50'
: 'border-gray-200 bg-white hover:border-blue-400 hover:bg-gray-50',
].join(' ')}
{...rest}
>
<span className="font-semibold text-gray-900">{name}</span>
<span className="text-sm text-gray-500">{description}</span>
</button>
);
}
```
**src/components/ui/PasswordField.tsx**
Password input with a show/hide eye toggle. Per locked decision: all password-type fields (access key, SAS URL, S3 secret) have a show/hide toggle. Per project instructions: each field has its own independent visibility state.
Props: `id` (string), `label` (string), `error` (FieldError | undefined), `registration` (return value of react-hook-form `register()`), `placeholder` (optional).
```tsx
import { useState } from 'react';
import type { FieldError, UseFormRegisterReturn } from 'react-hook-form';
interface PasswordFieldProps {
id: string;
label: string;
error?: FieldError;
registration: UseFormRegisterReturn;
placeholder?: string;
helpText?: string;
}
export function PasswordField({ id, label, error, registration, placeholder, helpText }: PasswordFieldProps) {
const [show, setShow] = useState(false);
return (
<div className="flex flex-col gap-1">
<label htmlFor={id} className="text-sm font-medium text-gray-700">
{label}
</label>
<div className="relative">
<input
id={id}
type={show ? 'text' : 'password'}
placeholder={placeholder}
className={[
'w-full rounded-md border px-3 py-2 pr-10 text-sm focus:outline-none focus:ring-2',
error ? 'border-red-500 focus:ring-red-300' : 'border-gray-300 focus:ring-blue-300',
].join(' ')}
{...registration}
/>
<button
type="button"
onClick={() => setShow(v => !v)}
aria-label={show ? 'Hide' : 'Show'}
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-700"
>
{show ? '🙈' : '👁'}
</button>
</div>
{helpText && !error && <p className="text-xs text-gray-500">{helpText}</p>}
{error && <p className="text-xs text-red-600">{error.message}</p>}
</div>
);
}
```
</action>
<verify>
<automated>npx vitest run --reporter=verbose 2>&1 | tail -20</automated>
</verify>
<done>BackendCard.tsx and PasswordField.tsx exist with correct exports. Vitest still reports RED (test stubs still fail — components exist but tests have expect.fail).</done>
</task>
<task type="auto">
<name>Task 2: Create FieldRenderer and AzureAuthToggle</name>
<files>src/components/ui/FieldRenderer.tsx, src/components/wizard/AzureAuthToggle.tsx</files>
<action>
**src/components/ui/FieldRenderer.tsx**
Renders a single `FieldDef` from the BACKEND_REGISTRY as the appropriate input element. This is the registry-driven form engine — no hardcoded per-backend JSX is allowed anywhere else.
The `provider` select field (s3 and s3-compatible backends) has only one option — hide it from the user and auto-register it with its default value. This follows the recommendation from 03-RESEARCH.md open question #2.
Props: `field` (FieldDef), `register` (UseFormRegister<any>), `error` (FieldError | undefined), `defaultValue` (string | undefined — used to auto-register hidden fields).
```tsx
import type { UseFormRegister, FieldError } from 'react-hook-form';
import type { FieldDef } from '../../schemas/registry';
import { PasswordField } from './PasswordField';
interface FieldRendererProps {
field: FieldDef;
register: UseFormRegister<any>;
error?: FieldError;
}
export function FieldRenderer({ field, register, error }: FieldRendererProps) {
// provider field: single-option select — hide from UI, auto-register with default value
if (field.key === 'provider' && field.options?.length === 1) {
return (
<input
type="hidden"
value={field.options[0].value}
{...register(field.key)}
/>
);
}
if (field.inputType === 'password') {
return (
<PasswordField
id={field.key}
label={field.label}
error={error}
registration={register(field.key)}
placeholder={field.placeholder}
helpText={field.helpText}
/>
);
}
if (field.inputType === 'select' && field.options) {
return (
<div className="flex flex-col gap-1">
<label htmlFor={field.key} className="text-sm font-medium text-gray-700">
{field.label}
{field.required && <span className="ml-1 text-red-500">*</span>}
</label>
<select
id={field.key}
className={[
'w-full rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-2',
error ? 'border-red-500 focus:ring-red-300' : 'border-gray-300 focus:ring-blue-300',
].join(' ')}
{...register(field.key)}
>
{field.options.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
{field.helpText && !error && <p className="text-xs text-gray-500">{field.helpText}</p>}
{error && <p className="text-xs text-red-600">{error.message}</p>}
</div>
);
}
// text (default)
return (
<div className="flex flex-col gap-1">
<label htmlFor={field.key} className="text-sm font-medium text-gray-700">
{field.label}
{field.required && <span className="ml-1 text-red-500">*</span>}
</label>
<input
id={field.key}
type="text"
placeholder={field.placeholder}
className={[
'w-full rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-2',
error ? 'border-red-500 focus:ring-red-300' : 'border-gray-300 focus:ring-blue-300',
].join(' ')}
{...register(field.key)}
/>
{field.helpText && !error && <p className="text-xs text-gray-500">{field.helpText}</p>}
{error && <p className="text-xs text-red-600">{error.message}</p>}
</div>
);
}
```
**src/components/wizard/AzureAuthToggle.tsx**
Azure-specific segmented control that toggles between "SAS URL" and "Access Key". Per locked decision:
- Default method: SAS URL
- Switching PRESERVES both field values (both are always registered in react-hook-form)
- Only the active field is displayed
- Both values flow into `remote.params` on form submit (generator strips inactive one)
Props: `register` (UseFormRegister<any>), `errors` (object with key and sas_url FieldError entries).
```tsx
import { useState } from 'react';
import type { UseFormRegister, FieldError } from 'react-hook-form';
import { PasswordField } from '../ui/PasswordField';
type AuthMethod = 'sas' | 'key';
interface AzureAuthToggleProps {
register: UseFormRegister<any>;
errors: {
key?: FieldError;
sas_url?: FieldError;
};
}
export function AzureAuthToggle({ register, errors }: AzureAuthToggleProps) {
const [authMethod, setAuthMethod] = useState<AuthMethod>('sas');
return (
<div className="flex flex-col gap-3">
{/* Segmented control */}
<div className="flex rounded-md border border-gray-300 overflow-hidden">
<button
type="button"
onClick={() => setAuthMethod('sas')}
className={[
'flex-1 py-1.5 text-sm font-medium transition-colors',
authMethod === 'sas'
? 'bg-blue-600 text-white'
: 'bg-white text-gray-700 hover:bg-gray-50',
].join(' ')}
>
SAS URL
</button>
<button
type="button"
onClick={() => setAuthMethod('key')}
className={[
'flex-1 py-1.5 text-sm font-medium transition-colors',
authMethod === 'key'
? 'bg-blue-600 text-white'
: 'bg-white text-gray-700 hover:bg-gray-50',
].join(' ')}
>
Access Key
</button>
</div>
{/* Both fields always registered — only active one visible */}
<div className={authMethod === 'sas' ? 'block' : 'hidden'}>
<PasswordField
id="sas_url"
label="SAS URL"
error={errors.sas_url}
registration={register('sas_url')}
placeholder="https://mystorageaccount.blob.core.windows.net/?sv=..."
helpText="Full SAS URL including account and container"
/>
</div>
<div className={authMethod === 'key' ? 'block' : 'hidden'}>
<PasswordField
id="key"
label="Access Key"
error={errors.key}
registration={register('key')}
helpText="Base64-encoded storage account key"
/>
</div>
</div>
);
}
```
CRITICAL: Using CSS `hidden` class (not conditional rendering) ensures both fields are registered with react-hook-form and their values are preserved when toggling. Do not use `{authMethod === 'sas' && <PasswordField ... />}` — that unmounts the hidden field and react-hook-form loses its value.
</action>
<verify>
<automated>npx vitest run --reporter=verbose 2>&1 | tail -20</automated>
</verify>
<done>
FieldRenderer.tsx and AzureAuthToggle.tsx exist with correct exports. All four atoms exist. Vitest still RED (stubs still have expect.fail — implementation tasks come in Plans 03 and 04).
</done>
</task>
</tasks>
<verification>
Run after both tasks complete:
```bash
npx vitest run --reporter=verbose 2>&1
```
Vitest runs cleanly (no crashes). Tests remain RED from stubs. All four atom files exist with named exports.
Check exports:
```bash
grep -n "^export" src/components/ui/BackendCard.tsx src/components/ui/PasswordField.tsx src/components/ui/FieldRenderer.tsx src/components/wizard/AzureAuthToggle.tsx
```
</verification>
<success_criteria>
- BackendCard, PasswordField, FieldRenderer, AzureAuthToggle all export their named components
- FieldRenderer hides the `provider` field for single-option selects and auto-registers the default value
- AzureAuthToggle registers BOTH `key` and `sas_url` with react-hook-form regardless of active method (uses CSS show/hide, not conditional rendering)
- PasswordField has per-instance show/hide state (not a global toggle)
- No new Tailwind config file created (Tailwind v4 CSS-first — all styles use utility classes directly)
</success_criteria>
<output>
After completion, create `.planning/phases/03-wizard-ui/03-02-SUMMARY.md`
</output>
+210
View File
@@ -0,0 +1,210 @@
---
phase: 03-wizard-ui
plan: "03"
type: execute
wave: 3
depends_on:
- "03-01"
- "03-02"
files_modified:
- src/components/wizard/BackendSelectionStep.tsx
autonomous: true
requirements:
- WIZD-01
- WIZD-04
must_haves:
truths:
- "BackendSelectionStep renders Azure Blob, Amazon S3, and S3-Compatible backend cards in that order"
- "Clicking a backend card dispatches SET_BACKEND_TYPE + SET_REMOTE_PARAMS({}) + SET_STEP(1) — no Next button needed"
- "Remote name field is rendered at the top of the step before the backend cards"
- "Remote name field validates alphanumeric/dash/underscore only — inline error shown only after first Next attempt"
- "Entering an invalid remote name and clicking a card does not advance (validation fires first)"
artifacts:
- path: "src/components/wizard/BackendSelectionStep.tsx"
provides: "Step 0 — remote name + backend card grid"
exports: ["BackendSelectionStep"]
key_links:
- from: "src/components/wizard/BackendSelectionStep.tsx"
to: "src/store/context.tsx"
via: "useWizard() for state and dispatch"
pattern: "useWizard"
- from: "src/components/wizard/BackendSelectionStep.tsx"
to: "src/components/ui/BackendCard.tsx"
via: "renders three BackendCard instances"
pattern: "BackendCard"
- from: "src/components/wizard/BackendSelectionStep.tsx"
to: "src/store/types.ts"
via: "dispatches SET_BACKEND_TYPE, SET_REMOTE_PARAMS, SET_STEP"
pattern: "dispatch.*SET_BACKEND_TYPE|SET_REMOTE_PARAMS|SET_STEP"
---
<objective>
Implement BackendSelectionStep — the first wizard step where the user sets a remote name and selects a storage backend.
Purpose: Satisfies WIZD-01 (popularity-sorted card grid) and WIZD-04 (remote name validation).
Output: src/components/wizard/BackendSelectionStep.tsx, BackendSelectionStep tests GREEN.
</objective>
<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>
<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.md
<interfaces>
<!-- Key types and contracts the executor needs. -->
From src/store/types.ts:
```typescript
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:
```typescript
export function useWizard(): { state: WizardState; dispatch: React.Dispatch<WizardAction> }
```
From src/components/ui/BackendCard.tsx (created in Plan 02):
```typescript
export function BackendCard(props: {
name: string;
description: string;
selected?: boolean;
onClick: () => void;
}): JSX.Element
```
Remote name validation regex (from RESEARCH.md):
```typescript
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):
```typescript
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 },
});
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Implement BackendSelectionStep and make its tests GREEN</name>
<files>src/components/wizard/BackendSelectionStep.tsx</files>
<behavior>
- WIZD-01: Renders Azure Blob, Amazon S3, S3-Compatible cards in that exact DOM order
- WIZD-01: Azure Blob card appears before S3 card in the DOM (popularity-sorted)
- WIZD-01: Clicking a card dispatches SET_BACKEND_TYPE with the correct BackendType value
- WIZD-01: Clicking a card also dispatches SET_REMOTE_PARAMS({}) to clear stale params (CRITICAL — reducer does not auto-clear)
- WIZD-01: Clicking a card dispatches SET_STEP(1) to advance after dispatching backend type
- WIZD-04: Remote name input is rendered before the backend cards in DOM order
- WIZD-04: No error message shown before the user has attempted to submit (mode: 'onSubmit')
- WIZD-04: After submit attempt with empty name, "Remote name is required" error shows below the input
- WIZD-04: After submit attempt with "my remote!", "Only letters, numbers, dashes, and underscores allowed" error shows
- WIZD-04: "my-remote_01" passes validation (alphanumeric, dashes, underscores allowed)
- WIZD-04: Clicking a card validates the name first — if invalid, error shows but navigation does NOT proceed
</behavior>
<action>
Create `src/components/wizard/BackendSelectionStep.tsx`.
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)`
</action>
<verify>
<automated>npx vitest run src/components/wizard/BackendSelectionStep.test.tsx --reporter=verbose 2>&1</automated>
</verify>
<done>
All BackendSelectionStep tests pass GREEN. WIZD-01 and WIZD-04 test cases are all green. BackendSelectionStep.tsx exports the component. The component correctly dispatches the three required actions on card click with valid name.
</done>
</task>
</tasks>
<verification>
```bash
npx vitest run src/components/wizard/BackendSelectionStep.test.tsx --reporter=verbose 2>&1
```
All WIZD-01 and WIZD-04 tests GREEN. Then run full suite:
```bash
npx vitest run 2>&1 | tail -10
```
Other test stubs remain RED (expected). BackendSelectionStep tests all GREEN.
</verification>
<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>
<output>
After completion, create `.planning/phases/03-wizard-ui/03-03-SUMMARY.md`
</output>
+299
View File
@@ -0,0 +1,299 @@
---
phase: 03-wizard-ui
plan: "04"
type: execute
wave: 3
depends_on:
- "03-01"
- "03-02"
files_modified:
- src/components/wizard/RemoteConfigStep.tsx
autonomous: true
requirements:
- BACK-01
- BACK-02
- BACK-03
must_haves:
truths:
- "RemoteConfigStep renders the correct fields for azureblob backend using BACKEND_REGISTRY iteration"
- "RemoteConfigStep renders the correct fields for s3 backend using BACKEND_REGISTRY iteration"
- "RemoteConfigStep renders the correct fields for s3-compatible backend including an endpoint field"
- "AzureAuthToggle appears for azureblob backend — SAS URL shown by default, toggle switches to Access Key"
- "Toggling Azure auth method does not clear the hidden field value (both registered)"
- "Errors only show after first Next attempt, then update live (mode: onSubmit, reValidateMode: onChange)"
- "RemoteConfigStep component is keyed on backendType to force remount on backend change"
artifacts:
- path: "src/components/wizard/RemoteConfigStep.tsx"
provides: "Step 1 — registry-driven backend configuration form"
exports: ["RemoteConfigStep"]
key_links:
- from: "src/components/wizard/RemoteConfigStep.tsx"
to: "src/schemas/registry.ts"
via: "BACKEND_REGISTRY[backendType] drives field rendering loop"
pattern: "BACKEND_REGISTRY"
- from: "src/components/wizard/RemoteConfigStep.tsx"
to: "src/schemas/index.ts"
via: "BACKEND_SCHEMAS[backendType] provides Zod resolver"
pattern: "BACKEND_SCHEMAS"
- from: "src/components/wizard/RemoteConfigStep.tsx"
to: "src/store/context.tsx"
via: "dispatches SET_REMOTE_PARAMS on Next, SET_STEP(2)"
pattern: "dispatch.*SET_REMOTE_PARAMS|SET_STEP"
---
<objective>
Implement RemoteConfigStep — the second wizard step with a registry-driven form for configuring the selected backend.
Purpose: Satisfies BACK-01 (Azure Blob config with auth toggle), BACK-02 (S3 config), BACK-03 (S3-compatible with endpoint).
Output: src/components/wizard/RemoteConfigStep.tsx, RemoteConfigStep tests GREEN.
</objective>
<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>
<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.md
<interfaces>
<!-- Key types and contracts the executor needs. -->
From src/schemas/registry.ts:
```typescript
export type BackendType = 'azureblob' | 's3' | 's3-compatible';
export const BACKEND_REGISTRY: Record<BackendType, FieldDef[]>
// azureblob fields: account (text, required), key (password, optional), sas_url (password, optional)
// s3 fields: provider (select/hidden), access_key_id (text), secret_access_key (password), region (text)
// s3-compatible fields: provider (select/hidden), access_key_id, secret_access_key, endpoint (text, required), region (text, optional)
```
From src/schemas/index.ts:
```typescript
export const BACKEND_SCHEMAS = {
azureblob: ZodObject,
s3: ZodObject,
's3-compatible': ZodObject,
} as const;
// All schemas built from BACKEND_REGISTRY — key/sas_url are optional (z.string().optional())
```
From src/store/types.ts:
```typescript
// On Next: dispatch SET_REMOTE_PARAMS with all form values
// Then: dispatch SET_STEP(2)
dispatch({ type: 'SET_REMOTE_PARAMS', payload: values });
dispatch({ type: 'SET_STEP', payload: 2 });
```
From src/components/ui/FieldRenderer.tsx (Plan 02):
```typescript
export function FieldRenderer(props: {
field: FieldDef;
register: UseFormRegister<any>;
error?: FieldError;
}): JSX.Element
// Handles text, password, select, hidden (provider single-option)
```
From src/components/wizard/AzureAuthToggle.tsx (Plan 02):
```typescript
export function AzureAuthToggle(props: {
register: UseFormRegister<any>;
errors: { key?: FieldError; sas_url?: FieldError };
}): JSX.Element
// Renders segmented SAS/Key toggle with both fields always registered
```
react-hook-form registry-driven pattern (from RESEARCH.md):
```tsx
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema),
mode: 'onSubmit',
reValidateMode: 'onChange',
defaultValues: state.remote.params,
});
const onNext = (values: Record<string, string>) => {
dispatch({ type: 'SET_REMOTE_PARAMS', payload: values });
dispatch({ type: 'SET_STEP', payload: 2 });
};
```
Remount on backend change (from RESEARCH.md Pitfall 1):
```tsx
// In App.tsx (or wherever RemoteConfigStep is rendered):
<RemoteConfigStep key={state.remote.backendType} />
// This forces full remount when backend changes — prevents stale defaultValues
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Implement RemoteConfigStep and make its tests GREEN</name>
<files>src/components/wizard/RemoteConfigStep.tsx</files>
<behavior>
- BACK-01: When backendType is 'azureblob', renders the 'account' text input
- BACK-01: When backendType is 'azureblob', renders AzureAuthToggle (not separate key/sas_url FieldRenderer calls)
- BACK-01: Azure form submits with both sas_url and key values in params (even if one is empty string)
- BACK-01: Toggling auth method in AzureAuthToggle does not remove the hidden field from form state
- BACK-02: When backendType is 's3', renders access_key_id, secret_access_key, and region inputs
- BACK-02: S3 provider field is hidden (auto-registered with value 'AWS') — no visible dropdown
- BACK-03: When backendType is 's3-compatible', renders endpoint field in addition to access_key_id, secret_access_key
- BACK-03: S3-compatible provider field is hidden (auto-registered with value 'Other')
- All backends: Errors do not show before first Next attempt (mode: 'onSubmit')
- All backends: Errors show after first failed Next attempt and update live (reValidateMode: 'onChange')
- All backends: Clicking Next with valid data dispatches SET_REMOTE_PARAMS then SET_STEP(2)
</behavior>
<action>
Create `src/components/wizard/RemoteConfigStep.tsx`.
The component reads `state.remote.backendType` and `state.remote.params` from `useWizard()`. It must NOT render if `backendType` is null (user somehow reached step 1 without selecting — guard with early return or redirect to step 0).
For the Azure backend, the field rendering is SPECIAL — do not loop `BACKEND_REGISTRY['azureblob']` naively:
- Render `account` field via `FieldRenderer`
- Render `AzureAuthToggle` for `key` / `sas_url` (handles both fields internally)
- Do NOT pass key/sas_url through the generic FieldRenderer loop for azureblob
For S3 and S3-compatible, loop all `BACKEND_REGISTRY[backendType]` fields through `FieldRenderer`. The `provider` field with a single option is auto-hidden by `FieldRenderer` already.
Implementation structure:
```tsx
export function RemoteConfigStep() {
const { state, dispatch } = useWizard();
const backendType = state.remote.backendType;
// Guard — should never happen but prevents runtime errors
if (!backendType) {
dispatch({ type: 'SET_STEP', payload: 0 });
return null;
}
const schema = BACKEND_SCHEMAS[backendType];
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema),
mode: 'onSubmit',
reValidateMode: 'onChange',
defaultValues: state.remote.params,
});
const onNext = (values: Record<string, string>) => {
dispatch({ type: 'SET_REMOTE_PARAMS', payload: values });
dispatch({ type: 'SET_STEP', payload: 2 });
};
const backendLabel = {
azureblob: 'Azure Blob Storage',
s3: 'Amazon S3',
's3-compatible': 'S3-Compatible Storage',
}[backendType];
return (
<div>
<h2>Step 2: Configure {backendLabel}</h2>
<form onSubmit={handleSubmit(onNext)}>
{backendType === 'azureblob' ? (
<>
{/* Account field via FieldRenderer */}
<FieldRenderer
field={BACKEND_REGISTRY.azureblob.find(f => f.key === 'account')!}
register={register}
error={errors.account}
/>
{/* Auth toggle handles key + sas_url — both always registered */}
<AzureAuthToggle
register={register}
errors={{ key: errors.key, sas_url: errors.sas_url }}
/>
</>
) : (
/* S3 and S3-compatible: full registry loop — FieldRenderer handles provider hiding */
BACKEND_REGISTRY[backendType].map(field => (
<FieldRenderer
key={field.key}
field={field}
register={register}
error={errors[field.key]}
/>
))
)}
<div className="flex gap-3 mt-6">
<button
type="button"
onClick={() => dispatch({ type: 'SET_STEP', payload: 0 })}
className="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50"
>
Back
</button>
<button
type="submit"
className="px-4 py-2 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700"
>
Next
</button>
</div>
</form>
</div>
);
}
```
Now update `src/components/wizard/RemoteConfigStep.test.tsx` to make all tests GREEN. Tests wrap component in `WizardProvider`. To set the backendType before rendering, create a helper that renders with a specific initial step/backend — dispatch actions in a test wrapper component, or initialize a custom context.
Practical test approach: Create a `TestWrapper` helper inside the test file that wraps with `WizardProvider` and dispatches the desired initial state before rendering `RemoteConfigStep`:
```tsx
function renderWithBackend(backendType: BackendType) {
function Setup() {
const { dispatch } = useWizard();
useEffect(() => {
dispatch({ type: 'SET_BACKEND_TYPE', payload: backendType });
dispatch({ type: 'SET_STEP', payload: 1 });
}, []);
return <RemoteConfigStep key={backendType} />;
}
return render(<WizardProvider><Setup /></WizardProvider>);
}
```
</action>
<verify>
<automated>npx vitest run src/components/wizard/RemoteConfigStep.test.tsx --reporter=verbose 2>&1</automated>
</verify>
<done>
All RemoteConfigStep tests pass GREEN. BACK-01 (Azure form with toggle), BACK-02 (S3 fields), BACK-03 (S3-compatible with endpoint) all green. RemoteConfigStep.tsx exports the component correctly.
</done>
</task>
</tasks>
<verification>
```bash
npx vitest run src/components/wizard/RemoteConfigStep.test.tsx --reporter=verbose 2>&1
```
All BACK-01, BACK-02, BACK-03 tests GREEN.
Full suite check:
```bash
npx vitest run 2>&1 | tail -10
```
RemoteConfigStep tests GREEN. BackendSelectionStep tests GREEN (from Plan 03). App and StepIndicator stubs still RED (expected — Plan 05).
</verification>
<success_criteria>
- RemoteConfigStep.tsx exists with named export
- Azure Blob form: account field + AzureAuthToggle renders (both key and sas_url registered, only one visible)
- S3 form: access_key_id, secret_access_key, region fields render; provider field is hidden
- S3-compatible form: same as S3 plus endpoint field renders; provider field is hidden
- Touch-then-live validation: no errors on initial render, errors after first failed submit
- On valid submit: dispatches SET_REMOTE_PARAMS with all field values (including hidden auth field), then SET_STEP(2)
- All BACK-01, BACK-02, BACK-03 tests GREEN
</success_criteria>
<output>
After completion, create `.planning/phases/03-wizard-ui/03-04-SUMMARY.md`
</output>
+321
View File
@@ -0,0 +1,321 @@
---
phase: 03-wizard-ui
plan: "05"
type: execute
wave: 4
depends_on:
- "03-03"
- "03-04"
files_modified:
- src/components/wizard/DeploymentStep.tsx
- src/components/wizard/StepIndicator.tsx
- src/App.tsx
autonomous: false
requirements:
- WIZD-02
- WIZD-03
must_haves:
truths:
- "App.tsx renders BackendSelectionStep for currentStep 0, RemoteConfigStep for step 1, DeploymentStep for step 2"
- "StepIndicator shows 3 labeled steps with checkmarks on completed steps"
- "Clicking a completed step in StepIndicator dispatches SET_STEP — clicking step 0 also dispatches SET_REMOTE_PARAMS({})"
- "DeploymentStep renders includeInstall toggle, configPath radio group, and scriptTargets checkboxes"
- "DeploymentStep dispatches SET_DEPLOYMENT on each change, keeping deployment state in sync"
- "Full forward + backward wizard navigation works without losing entered data in any step"
artifacts:
- path: "src/components/wizard/DeploymentStep.tsx"
provides: "Step 2 — deployment options (includeInstall, configPath, scriptTargets)"
exports: ["DeploymentStep"]
- path: "src/components/wizard/StepIndicator.tsx"
provides: "Breadcrumb navigation — 1.Backend > 2.Remote Config > 3.Deployment"
exports: ["StepIndicator"]
- path: "src/App.tsx"
provides: "Step router — renders correct step component by currentStep"
key_links:
- from: "src/App.tsx"
to: "src/components/wizard/BackendSelectionStep.tsx"
via: "STEPS[0] — renders when currentStep === 0"
pattern: "BackendSelectionStep"
- from: "src/App.tsx"
to: "src/components/wizard/RemoteConfigStep.tsx"
via: "STEPS[1] — renders when currentStep === 1, keyed on backendType"
pattern: "RemoteConfigStep"
- from: "src/components/wizard/StepIndicator.tsx"
to: "src/store/context.tsx"
via: "dispatches SET_STEP and conditionally SET_REMOTE_PARAMS on back-nav to step 0"
pattern: "dispatch.*SET_STEP|SET_REMOTE_PARAMS"
- from: "src/components/wizard/DeploymentStep.tsx"
to: "src/store/context.tsx"
via: "dispatches SET_DEPLOYMENT on every control change"
pattern: "dispatch.*SET_DEPLOYMENT"
---
<objective>
Wire the complete wizard: implement DeploymentStep and StepIndicator, then update App.tsx to route between all three steps and render the breadcrumb navigation.
Purpose: Satisfies WIZD-02 (multi-step navigation shell) and WIZD-03 (back navigation without data loss). Closes the full wizard loop.
Output: DeploymentStep, StepIndicator, App.tsx wired — all tests GREEN.
</objective>
<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>
<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-03-SUMMARY.md
@.planning/phases/03-wizard-ui/03-04-SUMMARY.md
<interfaces>
<!-- Key types and contracts the executor needs. -->
From src/store/types.ts:
```typescript
export interface WizardState {
currentStep: number; // 0=Backend, 1=RemoteConfig, 2=Deployment
remote: {
name: string;
backendType: BackendType | null;
params: Record<string, string>;
};
deployment: {
includeInstall: boolean; // toggle — default: false
configPath: 'machine-wide' | 'user-profile'; // radio — default: 'machine-wide'
scriptTargets: ('intune' | 'rmm')[]; // checkboxes — default: both selected
};
}
// Deploy step actions:
dispatch({ type: 'SET_DEPLOYMENT', payload: { includeInstall: true } });
dispatch({ type: 'SET_DEPLOYMENT', payload: { configPath: 'user-profile' } });
dispatch({ type: 'SET_DEPLOYMENT', payload: { scriptTargets: ['intune'] } });
// Back-nav to step 0 (CRITICAL: clears params but NOT deployment):
dispatch({ type: 'SET_REMOTE_PARAMS', payload: {} });
dispatch({ type: 'SET_STEP', payload: 0 });
```
From src/store/context.tsx:
```typescript
export function useWizard(): { state: WizardState; dispatch: React.Dispatch<WizardAction> }
```
Step routing pattern (from RESEARCH.md):
```tsx
// App.tsx step routing
import { BackendSelectionStep } from './components/wizard/BackendSelectionStep';
import { RemoteConfigStep } from './components/wizard/RemoteConfigStep';
import { DeploymentStep } from './components/wizard/DeploymentStep';
import { StepIndicator } from './components/wizard/StepIndicator';
// Key RemoteConfigStep on backendType to force remount on backend change (Pitfall 1 fix)
const STEPS = [
() => <BackendSelectionStep />,
() => <RemoteConfigStep key={state.remote.backendType} />,
() => <DeploymentStep />,
];
```
StepIndicator back-nav pattern (from RESEARCH.md):
```tsx
const handleStepClick = (targetStep: number) => {
if (targetStep === 0 && state.currentStep > 0) {
dispatch({ type: 'SET_REMOTE_PARAMS', payload: {} }); // clear params (backend may change)
// Do NOT dispatch RESET — deployment options must be preserved
}
dispatch({ type: 'SET_STEP', payload: targetStep });
};
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Implement DeploymentStep and StepIndicator</name>
<files>src/components/wizard/DeploymentStep.tsx, src/components/wizard/StepIndicator.tsx</files>
<behavior>
DeploymentStep:
- Renders an "Include rclone installation" toggle (checkbox or switch); state.deployment.includeInstall default false
- Renders a "Config deployment path" radio group: 'machine-wide' (C:\ProgramData\rclone\) and 'user-profile' (%APPDATA%\rclone\); default machine-wide
- Renders a "Script targets" checkbox group: 'intune' and 'rmm' checkboxes; both checked by default
- Each control dispatches SET_DEPLOYMENT immediately on change (live sync, no Next button needed for deployment options)
- Renders a "Back" button that dispatches SET_STEP(1) and a "Next / Review" button that dispatches SET_STEP(3) to advance to Phase 4's review step
StepIndicator:
- Renders three labeled steps: "1. Backend", "2. Remote Config", "3. Deployment"
- Current step is bold/active
- Completed steps (step index < currentStep) show a checkmark prefix and are clickable buttons
- Future steps are not clickable (non-interactive)
- Clicking a completed step that is NOT step 0 dispatches only SET_STEP(targetStep)
- Clicking step 0 dispatches SET_REMOTE_PARAMS({}) THEN SET_STEP(0) — clears params for potential backend change
- Does NOT dispatch RESET on any click
</behavior>
<action>
Create `src/components/wizard/DeploymentStep.tsx`:
- Uses `useWizard()` to read `state.deployment` and `dispatch`
- Does NOT use react-hook-form — deployment fields dispatch directly via onChange handlers
- `includeInstall` toggle: `<input type="checkbox" checked={state.deployment.includeInstall} onChange={e => dispatch({ type: 'SET_DEPLOYMENT', payload: { includeInstall: e.target.checked } })} />`
- `configPath` radios: two `<input type="radio">` elements for 'machine-wide' and 'user-profile'
- `scriptTargets` checkboxes: two `<input type="checkbox">` for 'intune' and 'rmm' — on change, compute new array and dispatch SET_DEPLOYMENT
- Back button: `dispatch({ type: 'SET_STEP', payload: 1 })`
- Next button: `dispatch({ type: 'SET_STEP', payload: 3 })` — step 3 is Phase 4's review/download (placeholder for now)
Create `src/components/wizard/StepIndicator.tsx`:
- Uses `useWizard()` to read `state.currentStep`
- Renders a horizontal breadcrumb with separators ()
- Step labels: `['Backend', 'Remote Config', 'Deployment']`
- For each step index i:
- If i < currentStep: completed — render as `<button>` with "checkmark {label}"
- If i === currentStep: active — render as `<span>` bold with "{i+1}. {label}"
- If i > currentStep: future — render as `<span>` muted with "{i+1}. {label}"
- `handleStepClick(i)`: if i === 0 and currentStep > 0, dispatch SET_REMOTE_PARAMS({}) first, then dispatch SET_STEP(i)
Update `src/components/wizard/StepIndicator.test.tsx` to make all WIZD-03 tests GREEN:
- "clicking a completed step dispatches SET_STEP" — render with currentStep=2, click "Backend" step, verify SET_STEP(0) was dispatched (use spy or observe re-render)
- "clicking back to step 0 dispatches SET_REMOTE_PARAMS({})" — same scenario, verify params are cleared
- "clicking back to step 0 does NOT dispatch RESET" — verify no full RESET
- "step 0 shows as active when currentStep is 0" — render with step 0
- "completed steps are clickable" — render with step 2, verify step 0 and 1 have button role
</action>
<verify>
<automated>npx vitest run src/components/wizard/StepIndicator.test.tsx --reporter=verbose 2>&1</automated>
</verify>
<done>
DeploymentStep.tsx and StepIndicator.tsx exist with named exports. All StepIndicator WIZD-03 tests GREEN.
</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Wire App.tsx step router and make App.test GREEN</name>
<files>src/App.tsx</files>
<behavior>
- App renders BackendSelectionStep when state.currentStep === 0
- App renders RemoteConfigStep when state.currentStep === 1
- App renders DeploymentStep when state.currentStep === 2
- App always renders StepIndicator above the current step component
- RemoteConfigStep is keyed on state.remote.backendType to force remount on backend change
</behavior>
<action>
Replace the placeholder `src/App.tsx` with the step router.
```tsx
import { useWizard } from './store/context';
import { WizardProvider } from './store/context';
import { StepIndicator } from './components/wizard/StepIndicator';
import { BackendSelectionStep } from './components/wizard/BackendSelectionStep';
import { RemoteConfigStep } from './components/wizard/RemoteConfigStep';
import { DeploymentStep } from './components/wizard/DeploymentStep';
function WizardShell() {
const { state } = useWizard();
const steps = [
<BackendSelectionStep />,
<RemoteConfigStep key={state.remote.backendType ?? 'none'} />,
<DeploymentStep />,
];
// Guard: clamp to valid range (phase 4 adds step 3 later)
const stepIndex = Math.min(state.currentStep, steps.length - 1);
const CurrentStep = steps[stepIndex];
return (
<div className="min-h-screen bg-gray-50 flex flex-col items-center py-12 px-4">
<div className="w-full max-w-2xl">
<h1 className="text-3xl font-bold text-gray-900 mb-8 text-center">Ready2Blob</h1>
<StepIndicator />
<div className="mt-8">
{CurrentStep}
</div>
</div>
</div>
);
}
export default function App() {
return (
<WizardProvider>
<WizardShell />
</WizardProvider>
);
}
```
Then update `src/App.test.tsx` to make all WIZD-02 tests GREEN:
- "renders BackendSelectionStep when currentStep is 0" — render App, expect to find backend card grid (e.g., text "Azure Blob Storage")
- "renders RemoteConfigStep when currentStep is 1" — render App, dispatch SET_BACKEND_TYPE + SET_STEP(1), expect backend config form to appear
- "renders DeploymentStep when currentStep is 2" — render App, navigate to step 2, expect deployment options to appear
Testing approach: `render(<App />)`, then use a dispatch helper or find elements that are only present on the target step.
</action>
<verify>
<automated>npx vitest run src/App.test.tsx --reporter=verbose 2>&1</automated>
</verify>
<done>
All App.test.tsx WIZD-02 tests GREEN. App.tsx wires all three steps. StepIndicator renders above each step.
</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 3: Human verification of complete wizard flow end-to-end</name>
<action>Human runs `npm run dev` and manually verifies the complete wizard flow in the browser. No code changes required — this task only requires visual inspection and interaction testing.</action>
<what-built>Complete wizard navigation: BackendSelectionStep to RemoteConfigStep to DeploymentStep, with StepIndicator breadcrumb and full back-navigation support.</what-built>
<how-to-verify>
1. Run `npm run dev` and open http://localhost:5173
2. Verify the breadcrumb shows "1. Backend 2. Remote Config 3. Deployment"
3. Step 1 (Backend Selection):
- Remote name field is at the top
- Try clicking a card without a name — inline error should appear below the name field
- Enter "my-remote" in the name field, click "Azure Blob Storage" — should advance to step 2
4. Step 2 (Remote Config) with Azure:
- Storage Account Name field visible
- "SAS URL" is the default active auth method
- Enter a SAS URL, toggle to "Access Key", enter a key — both values should be in the form
- Click "Back" — should return to step 1 with "my-remote" still in the name field
5. Navigate back to step 1, click "Amazon S3" — should advance to step 2 with S3 fields
6. Verify S3 form: access key ID, secret access key, region fields (no provider dropdown visible)
7. Fill S3 form and click "Next" — should advance to step 3
8. Step 3 (Deployment): verify include-install toggle, config path radios, script targets checkboxes
9. Click step "1. Backend" in breadcrumb from step 3 — should navigate back to step 1 without losing deployment options
10. Select a different backend — step 2 form should be blank (not showing old S3 values)
</how-to-verify>
<verify>
<automated>npm run dev 2>&1 | head -5</automated>
</verify>
<done>User confirms the full wizard flow works end-to-end with correct navigation, validation, and data preservation.</done>
<resume-signal>Type "approved" if navigation works correctly end-to-end, or describe any issues found.</resume-signal>
</task>
</tasks>
<verification>
Full automated suite:
```bash
npx vitest run --reporter=verbose 2>&1
```
All tests GREEN: App.test.tsx (WIZD-02), BackendSelectionStep.test.tsx (WIZD-01, WIZD-04), RemoteConfigStep.test.tsx (BACK-01, BACK-02, BACK-03), StepIndicator.test.tsx (WIZD-03).
Dev server check:
```bash
npm run dev 2>&1 | head -5
```
Server starts without errors.
</verification>
<success_criteria>
- All 7 requirement test suites GREEN (WIZD-01, WIZD-02, WIZD-03, WIZD-04, BACK-01, BACK-02, BACK-03)
- App.tsx routes to correct step component by currentStep
- RemoteConfigStep keyed on backendType (prevents stale form values on backend change)
- StepIndicator dispatches SET_REMOTE_PARAMS({}) when navigating back to step 0 (preserves deployment)
- DeploymentStep dispatches SET_DEPLOYMENT live on every control change
- Human verifies full wizard flow end-to-end: forward navigation, back navigation, data preservation
</success_criteria>
<output>
After completion, create `.planning/phases/03-wizard-ui/03-05-SUMMARY.md`
</output>
@@ -0,0 +1,107 @@
---
phase: 05-tech-debt
plan: "03"
subsystem: testing
tags: [vitest, userEvent, testing-library, act-warnings, fake-timers]
# Dependency graph
requires:
- phase: 05-tech-debt
provides: "BackendSelectionStep and ReviewStep test infrastructure from Plans 01-02"
provides:
- "act()-safe test interactions in BackendSelectionStep.test.tsx using userEvent v14"
- "act()-safe timer handling in ReviewStep.test.tsx using vi.useFakeTimers()"
affects: [05-tech-debt, 06-new-backends, 07-validation-ux]
# Tech tracking
tech-stack:
added: []
patterns:
- "userEvent.setup() + await user.click/type per test body (not module level)"
- "vi.useFakeTimers() in beforeEach + vi.useRealTimers() in afterEach"
key-files:
created: []
modified:
- src/components/wizard/BackendSelectionStep.test.tsx
- src/components/wizard/ReviewStep.test.tsx
key-decisions:
- "Replace fireEvent.click with await user.click (userEvent.setup() per test body) — eliminates act() wrapping requirement for async React Hook Form validation"
- "Keep fireEvent.change for input value setting replaced by user.clear() + user.type() — user.type() appends so clear() first required"
- "vi.useFakeTimers() in beforeEach freezes OutputBlock setTimeout(setCopied(false), 2000) preventing post-test act() warnings"
patterns-established:
- "userEvent pattern: const user = userEvent.setup() inside it() body, never at module level"
- "Fake timers pattern: vi.useFakeTimers() in beforeEach, vi.useRealTimers() in afterEach"
requirements-completed: [TECH-05]
# Metrics
duration: 3min
completed: 2026-03-30
---
# Phase 5 Plan 03: Act() Warning Elimination Summary
**Migrated BackendSelectionStep.test.tsx to userEvent v14 and added vi.useFakeTimers() to ReviewStep.test.tsx, eliminating all act() warnings from both test suites**
## Performance
- **Duration:** 3 min
- **Started:** 2026-03-30T07:40:33Z
- **Completed:** 2026-03-30T07:43:41Z
- **Tasks:** 2
- **Files modified:** 2
## Accomplishments
- Replaced all `fireEvent.click` / `fireEvent.change` calls in BackendSelectionStep.test.tsx with `userEvent.setup()` + `await user.click()` / `await user.type()`
- Added `vi.useFakeTimers()` to ReviewStep's `beforeEach` and `vi.useRealTimers()` to a new `afterEach` block
- Full suite: 104 tests passing across 12 test files with zero act() warnings and zero TypeScript errors
## Task Commits
Each task was committed atomically:
1. **Task 1: Migrate BackendSelectionStep.test.tsx from fireEvent to userEvent** - `9b6d8a8` (feat)
2. **Task 2: Fix ReviewStep act() warnings with vi.useFakeTimers** - `913cbe8` (feat)
**Plan metadata:** (docs commit — see final commit)
## Files Created/Modified
- `src/components/wizard/BackendSelectionStep.test.tsx` - Replaced fireEvent import with userEvent; 5 tests converted to async with userEvent.setup() pattern
- `src/components/wizard/ReviewStep.test.tsx` - Added afterEach to imports, vi.useFakeTimers() in beforeEach, new afterEach(() => vi.useRealTimers())
## Decisions Made
- Used `user.clear()` before `user.type()` in all input tests — userEvent.type() appends to existing value, so clear is required to avoid stale default values contaminating typed content
- Kept `fireEvent` removed entirely from BackendSelectionStep.test.tsx (not just partially replaced) — cleaner import, no risk of future misuse
- Added `vi.useFakeTimers()` to the existing `beforeEach` block (not a new one) per plan specification — avoids multiple beforeEach hooks
## Deviations from Plan
None - plan executed exactly as written.
## Issues Encountered
None — both migrations were straightforward. All 10 BackendSelectionStep tests and all 15 ReviewStep tests passed on first run after migration.
## User Setup Required
None - no external service configuration required.
## Next Phase Readiness
- TECH-05 requirement satisfied: CI test output is now clean with zero act() warnings
- All 5 TECH-0x requirements (TECH-01 through TECH-05) are verified green in the full test suite
- Phase 05-tech-debt test infrastructure is fully act()-safe — patterns established for future test files in phases 6 and 7
---
*Phase: 05-tech-debt*
*Completed: 2026-03-30*
## Self-Check: PASSED
- `src/components/wizard/BackendSelectionStep.test.tsx` - FOUND
- `src/components/wizard/ReviewStep.test.tsx` - FOUND
- `.planning/phases/05-tech-debt/05-03-SUMMARY.md` - FOUND
- Commit `9b6d8a8` (Task 1) - FOUND
- Commit `913cbe8` (Task 2) - FOUND