test(03-05): add failing StepIndicator WIZD-03 tests (RED)

- WIZD-03: clicking completed step dispatches SET_STEP
- WIZD-03: clicking back to step 0 dispatches SET_REMOTE_PARAMS({})
- WIZD-03: clicking back to step 0 does NOT dispatch RESET
- WIZD-03: step 0 shows as active when currentStep is 0
- WIZD-03: completed steps are clickable
This commit is contained in:
2026-03-27 09:42:45 +01:00
parent 3155491315
commit 7ffccfb7f8
+210 -11
View File
@@ -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 <StepIndicator />;
}
return render(
<WizardProvider>
<Wrapper />
</WizardProvider>
);
}
// 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<typeof dispatch>[0]);
},
[dispatch]
);
return (
<div>
<span data-testid="current-step">{state.currentStep}</span>
<StepIndicatorWithDispatch dispatch={spyDispatch} />
</div>
);
}
// We'll test dispatch behavior by checking actual state changes instead
return { dispatched, ...render(<WizardProvider><SpyWrapper /></WizardProvider>) };
}
// 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(
<WizardProvider>
<StepIndicator />
</WizardProvider>
);
// 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 <StepIndicator />;
}
render(
<WizardProvider>
<Wrapper />
</WizardProvider>
);
// 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 (
<>
<span data-testid="step">{state.currentStep}</span>
<StepIndicator />
</>
);
}
render(
<WizardProvider>
<Wrapper />
</WizardProvider>
);
// 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 (
<>
<span data-testid="params">{JSON.stringify(state.remote.params)}</span>
<StepIndicator />
</>
);
}
render(
<WizardProvider>
<Wrapper />
</WizardProvider>
);
// 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 (
<>
<span data-testid="include-install">{String(state.deployment.includeInstall)}</span>
<span data-testid="targets">{JSON.stringify(state.deployment.scriptTargets)}</span>
<StepIndicator />
</>
);
}
render(
<WizardProvider>
<Wrapper />
</WizardProvider>
);
// 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;
}