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:
@@ -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 03–05 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user