diff --git a/src/components/wizard/StepIndicator.test.tsx b/src/components/wizard/StepIndicator.test.tsx index 2d1d9b4..d5dcacb 100644 --- a/src/components/wizard/StepIndicator.test.tsx +++ b/src/components/wizard/StepIndicator.test.tsx @@ -1,27 +1,226 @@ // @vitest-environment jsdom // Covers WIZD-03: going back preserves remote.params; deployment options are untouched -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { StepIndicator } from './StepIndicator'; +import { WizardProvider } from '../../store/context'; +import { useWizard } from '../../store/context'; + +// Helper: renders StepIndicator with a given currentStep by navigating the wizard state +function renderWithStep(currentStep: number) { + // We need a wrapper that sets the step before rendering StepIndicator + function Wrapper() { + const { dispatch } = useWizard(); + React.useEffect(() => { + if (currentStep > 0) { + dispatch({ type: 'SET_STEP', payload: currentStep }); + } + }, [dispatch]); + return ; + } + + return render( + + + + ); +} + +// Helper: renders StepIndicator with a spy dispatch +function renderWithDispatchSpy(currentStep: number) { + const dispatched: { type: string; payload?: unknown }[] = []; + + function SpyWrapper() { + const { dispatch, state } = useWizard(); + React.useEffect(() => { + if (currentStep > 0) { + dispatch({ type: 'SET_STEP', payload: currentStep }); + } + }, [dispatch]); + + // Wrap dispatch to spy on calls after initial setup + const spyDispatch = React.useCallback( + (action: { type: string; payload?: unknown }) => { + dispatched.push(action); + dispatch(action as Parameters[0]); + }, + [dispatch] + ); + + return ( +
+ {state.currentStep} + +
+ ); + } + + // We'll test dispatch behavior by checking actual state changes instead + return { dispatched, ...render() }; +} + +// A version of StepIndicator that accepts an optional dispatch override for testing +// Actually we'll just test via rendered output and state changes describe('StepIndicator', () => { describe('WIZD-03: back navigation preserves state', () => { - it('clicking a completed step dispatches SET_STEP', () => { - expect.fail('not yet implemented'); + it('step 0 shows as active when currentStep is 0', async () => { + render( + + + + ); + // Step 0 (Backend) should be the active step — rendered as bold/active span + const backendLabel = screen.getByText(/Backend/); + expect(backendLabel).toBeDefined(); }); - it('clicking back to step 0 dispatches SET_REMOTE_PARAMS({}) to clear params', () => { - expect.fail('not yet implemented'); + it('completed steps are clickable', async () => { + // Render with currentStep=2 so steps 0 and 1 are completed + function Wrapper() { + const { dispatch } = useWizard(); + React.useEffect(() => { + dispatch({ type: 'SET_STEP', payload: 2 }); + }, [dispatch]); + return ; + } + + render( + + + + ); + + // Wait for useEffect to set step to 2 + await screen.findByText(/Deployment/); + + // Steps 0 and 1 should be clickable buttons + const buttons = screen.getAllByRole('button'); + // Should have at least 2 buttons for step 0 and step 1 + expect(buttons.length).toBeGreaterThanOrEqual(2); }); - it('clicking back to step 0 does NOT dispatch RESET (deployment preserved)', () => { - expect.fail('not yet implemented'); + it('clicking a completed step dispatches SET_STEP', async () => { + const user = userEvent.setup(); + + function Wrapper() { + const { dispatch, state } = useWizard(); + React.useEffect(() => { + dispatch({ type: 'SET_STEP', payload: 2 }); + }, [dispatch]); + return ( + <> + {state.currentStep} + + + ); + } + + render( + + + + ); + + // Wait until currentStep is 2 + await screen.findByText('2', { selector: '[data-testid="step"]' }); + + // Click the "Backend" step (step 0 — completed) + const buttons = screen.getAllByRole('button'); + const backendButton = buttons.find((b) => b.textContent?.includes('Backend')); + expect(backendButton).toBeDefined(); + await user.click(backendButton!); + + // After clicking, currentStep should be 0 + const stepEl = screen.getByTestId('step'); + expect(stepEl.textContent).toBe('0'); }); - it('step 0 shows as active when currentStep is 0', () => { - expect.fail('not yet implemented'); + it('clicking back to step 0 dispatches SET_REMOTE_PARAMS({}) to clear params', async () => { + const user = userEvent.setup(); + + function Wrapper() { + const { dispatch, state } = useWizard(); + React.useEffect(() => { + dispatch({ type: 'SET_BACKEND_TYPE', payload: 'azureblob' }); + dispatch({ type: 'SET_REMOTE_PARAMS', payload: { account: 'myaccount' } }); + dispatch({ type: 'SET_STEP', payload: 2 }); + }, [dispatch]); + return ( + <> + {JSON.stringify(state.remote.params)} + + + ); + } + + render( + + + + ); + + // Wait until step is 2 and params are set + await screen.findByText((text) => text.includes('myaccount'), { selector: '[data-testid="params"]' }); + + // Click the "Backend" step (step 0) + const buttons = screen.getAllByRole('button'); + const backendButton = buttons.find((b) => b.textContent?.includes('Backend')); + expect(backendButton).toBeDefined(); + await user.click(backendButton!); + + // params should be cleared + const paramsEl = screen.getByTestId('params'); + expect(paramsEl.textContent).toBe('{}'); }); - it('completed steps are clickable', () => { - expect.fail('not yet implemented'); + it('clicking back to step 0 does NOT dispatch RESET (deployment preserved)', async () => { + const user = userEvent.setup(); + + function Wrapper() { + const { dispatch, state } = useWizard(); + React.useEffect(() => { + // Set a non-default deployment state + dispatch({ type: 'SET_DEPLOYMENT', payload: { includeInstall: true } }); + dispatch({ type: 'SET_DEPLOYMENT', payload: { scriptTargets: ['intune'] } }); + dispatch({ type: 'SET_STEP', payload: 2 }); + }, [dispatch]); + return ( + <> + {String(state.deployment.includeInstall)} + {JSON.stringify(state.deployment.scriptTargets)} + + + ); + } + + render( + + + + ); + + // Wait until deployment state is customized and step is 2 + await screen.findByText('true', { selector: '[data-testid="include-install"]' }); + + // Click the "Backend" step + const buttons = screen.getAllByRole('button'); + const backendButton = buttons.find((b) => b.textContent?.includes('Backend')); + expect(backendButton).toBeDefined(); + await user.click(backendButton!); + + // Deployment options should be preserved (not reset) + const includeInstallEl = screen.getByTestId('include-install'); + expect(includeInstallEl.textContent).toBe('true'); + const targetsEl = screen.getByTestId('targets'); + expect(targetsEl.textContent).toBe('["intune"]'); }); }); }); + +// Placeholder type for the spy component — not actually used in tests +function StepIndicatorWithDispatch(_props: { dispatch: (action: { type: string; payload?: unknown }) => void }) { + return null; +}