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,39 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" commit \"docs: initialize project\" --files .planning/PROJECT.md)",
|
||||||
|
"Bash(node \"C:/Users/SebastienQUEROL/.claude/get-shit-done/bin/gsd-tools.cjs\" commit \"docs: complete project research\" --files .planning/research/)",
|
||||||
|
"WebSearch",
|
||||||
|
"Bash(npm test:*)",
|
||||||
|
"Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" phase complete \"02\")",
|
||||||
|
"Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" commit \"docs\\(phase-02\\): complete phase execution\" --files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md .planning/phases/02-generators/02-VERIFICATION.md)",
|
||||||
|
"Bash(npx vitest:*)",
|
||||||
|
"Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" state advance-plan)",
|
||||||
|
"Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" state update-progress)",
|
||||||
|
"Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" state record-metric --phase \"03-wizard-ui\" --plan \"05\" --duration \"5min\" --tasks \"3\" --files \"5\")",
|
||||||
|
"Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" state add-decision --phase \"03-wizard-ui\" --summary \"[03-05] Human verification approved — full 3-step wizard flow confirmed working end-to-end\")",
|
||||||
|
"Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" state record-session --stopped-at \"Completed 03-wizard-ui-03-05-PLAN.md\")",
|
||||||
|
"Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" roadmap update-plan-progress \"03\")",
|
||||||
|
"Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" requirements mark-complete WIZD-02 WIZD-03)",
|
||||||
|
"Bash(ls C:/Users/SebastienQUEROL/Documents/projets/Ready2Blob/.planning/phases/03-wizard-ui/*-VERIFICATION.md)",
|
||||||
|
"Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" commit \"docs\\(phase-03\\): complete phase execution\" --files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md .planning/phases/03-wizard-ui/03-VERIFICATION.md)",
|
||||||
|
"Bash(git add:*)",
|
||||||
|
"Bash(git commit:*)",
|
||||||
|
"Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" state record-metric --phase \"04-review-download-security\" --plan \"05\" --duration \"1min\" --tasks \"1\" --files \"0\")",
|
||||||
|
"Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" state add-decision --phase \"04-review-download-security\" --summary \"[04-05] Human verification approved — Phase 4 full feature set confirmed working end-to-end in real browser\")",
|
||||||
|
"Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" state record-session --stopped-at \"Completed 04-05-PLAN.md — Phase 4 human verification approved, all 18 checks passed\")",
|
||||||
|
"Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" roadmap update-plan-progress 4)",
|
||||||
|
"Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" requirements mark-complete CONF-02 CONF-03 DOWN-01 DOWN-02 DOWN-03 DOWN-04 DOWN-05 DOWN-06 SECU-01 SECU-02 SECU-03)",
|
||||||
|
"Bash(ls /c/Users/SebastienQUEROL/Documents/projets/Ready2Blob/.planning/phases/04-review-download-security/*VERIFICATION*)",
|
||||||
|
"Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" commit \"docs\\(phase-04\\): complete phase execution\" --files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md .planning/phases/04-review-download-security/04-VERIFICATION.md)",
|
||||||
|
"Bash(ls -d .planning/milestones/v*-phases)",
|
||||||
|
"Bash(ls -d .planning/phases/*/)",
|
||||||
|
"Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" milestone complete \"v1.0\" --name \"MVP\")",
|
||||||
|
"Bash(ls .planning/*.md)",
|
||||||
|
"Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" commit \"docs: start milestone v1.1 Backlog & Tech Debt\" --files .planning/PROJECT.md .planning/STATE.md)",
|
||||||
|
"Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" init new-milestone)",
|
||||||
|
"Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" config-set workflow.research true)",
|
||||||
|
"Bash(mkdir -p .planning/research)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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-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-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)
|
- [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`
|
- [x] **TECH-04**: Dead `BackendFormValues<T>` export is removed from `src/schemas/index.ts`
|
||||||
- [ ] **TECH-05**: BackendSelectionStep test suite runs without `act()` warnings
|
- [x] **TECH-05**: BackendSelectionStep test suite runs without `act()` warnings
|
||||||
|
|
||||||
### Backends
|
### Backends
|
||||||
|
|
||||||
@@ -49,8 +49,8 @@
|
|||||||
| TECH-01 | Phase 5 | Complete |
|
| TECH-01 | Phase 5 | Complete |
|
||||||
| TECH-02 | Phase 5 | Complete |
|
| TECH-02 | Phase 5 | Complete |
|
||||||
| TECH-03 | Phase 5 | Complete |
|
| TECH-03 | Phase 5 | Complete |
|
||||||
| TECH-04 | Phase 5 | Pending |
|
| TECH-04 | Phase 5 | Complete |
|
||||||
| TECH-05 | Phase 5 | Pending |
|
| TECH-05 | Phase 5 | Complete |
|
||||||
| BACK-01 | Phase 6 | Pending |
|
| BACK-01 | Phase 6 | Pending |
|
||||||
| BACK-02 | Phase 6 | Pending |
|
| BACK-02 | Phase 6 | Pending |
|
||||||
| BACK-03 | Phase 6 | Pending |
|
| BACK-03 | Phase 6 | Pending |
|
||||||
|
|||||||
@@ -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.
|
**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 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
|
- [ ] **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 |
|
| 2. Generators | v1.0 | 4/4 | Complete | 2026-03-26 |
|
||||||
| 3. Wizard UI | v1.0 | 5/5 | Complete | 2026-03-27 |
|
| 3. Wizard UI | v1.0 | 5/5 | Complete | 2026-03-27 |
|
||||||
| 4. Review, Download & Security | 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 | - |
|
| 6. New Backends | v1.1 | 0/TBD | Not started | - |
|
||||||
| 7. Validation & UX Polish | v1.1 | 0/TBD | Not started | - |
|
| 7. Validation & UX Polish | v1.1 | 0/TBD | Not started | - |
|
||||||
|
|||||||
+9
-6
@@ -3,14 +3,14 @@ gsd_state_version: 1.0
|
|||||||
milestone: v1.1
|
milestone: v1.1
|
||||||
milestone_name: Backlog & Tech Debt
|
milestone_name: Backlog & Tech Debt
|
||||||
status: planning
|
status: planning
|
||||||
stopped_at: Completed 05-tech-debt 05-01-PLAN.md
|
stopped_at: Completed 05-tech-debt 05-03-PLAN.md
|
||||||
last_updated: "2026-03-30T07:39:32.011Z"
|
last_updated: "2026-03-30T07:44:48.651Z"
|
||||||
last_activity: 2026-03-27 — v1.1 roadmap created, 11 requirements mapped across 3 phases
|
last_activity: 2026-03-27 — v1.1 roadmap created, 11 requirements mapped across 3 phases
|
||||||
progress:
|
progress:
|
||||||
total_phases: 3
|
total_phases: 3
|
||||||
completed_phases: 0
|
completed_phases: 1
|
||||||
total_plans: 4
|
total_plans: 4
|
||||||
completed_plans: 3
|
completed_plans: 4
|
||||||
percent: 0
|
percent: 0
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -51,6 +51,7 @@ Progress: [░░░░░░░░░░] 0% (v1.1)
|
|||||||
| Phase 05-tech-debt P00 | 3 | 2 tasks | 2 files |
|
| Phase 05-tech-debt P00 | 3 | 2 tasks | 2 files |
|
||||||
| Phase 05-tech-debt P02 | 4min | 1 tasks | 1 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 P01 | 5 | 2 tasks | 4 files |
|
||||||
|
| Phase 05-tech-debt P03 | 3 | 2 tasks | 2 files |
|
||||||
|
|
||||||
## Accumulated Context
|
## 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]: 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]: 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]: 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
|
### Pending Todos
|
||||||
|
|
||||||
@@ -82,6 +85,6 @@ None yet.
|
|||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-03-30T07:39:32.005Z
|
Last session: 2026-03-30T07:44:48.644Z
|
||||||
Stopped at: Completed 05-tech-debt 05-01-PLAN.md
|
Stopped at: Completed 05-tech-debt 05-03-PLAN.md
|
||||||
Resume file: None
|
Resume file: None
|
||||||
|
|||||||
@@ -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