Files
OpenNoodl/packages/noodl-editor/tests/models/ProjectCreationWizard.test.ts
Richard Osborne d9acb41d2f feat(launcher): add ProjectCreationWizard multi-step flow
Replaces CreateProjectModal with a guided wizard supporting:
- Quick Start (name + location, direct creation)
- Guided Setup (basics → style preset → review → create)
- AI Builder stub (V2, same flow as Guided for now)

Files added:
- WizardContext.tsx — state machine with step sequencing + canProceed validation
- EntryModeStep — card-based mode selector
- ProjectBasicsStep — name/description/location picker
- StylePresetStep — preset card grid
- ReviewStep — summary before creation
- ProjectCreationWizard.tsx — drop-in replacement for CreateProjectModal
- SCSS + index.ts barrel

Integration:
- ProjectsPage.tsx imports ProjectCreationWizard (API-identical swap)

Tests:
- tests/models/ProjectCreationWizard.test.ts — 17 pure logic tests
  (infra note: npm run test:editor required, not npx jest directly)

Pre-existing broken tests in tests/components/ and tests/git/ are
unrelated and not modified.
2026-02-18 16:45:12 +01:00

216 lines
6.4 KiB
TypeScript

/**
* ProjectCreationWizard — Unit tests for wizard state management
*
* Tests the step-sequencing logic and validation rules defined in WizardContext.
* These are pure logic tests — no DOM or React renderer required.
*
* The functions below mirror the private helpers in WizardContext.tsx.
* If the context logic changes, update both files.
*/
import { describe, it, expect } from '@jest/globals';
// ---- Step sequencing (mirrors WizardContext.getStepSequence) ---------------
function getStepSequence(mode) {
switch (mode) {
case 'quick':
return ['basics'];
case 'guided':
return ['basics', 'preset', 'review'];
case 'ai':
return ['basics', 'preset', 'review'];
default:
return ['basics'];
}
}
// ---- Validation (mirrors WizardContext.isStepValid) ------------------------
function isStepValid(step, state) {
switch (step) {
case 'entry':
return true;
case 'basics':
return state.projectName.trim().length > 0 && state.location.length > 0;
case 'preset':
return state.selectedPresetId.length > 0;
case 'review':
return true;
default:
return false;
}
}
// ---- Step navigation (mirrors WizardContext goNext/goBack logic) -----------
function goNext(state) {
if (state.currentStep === 'entry') {
const seq = getStepSequence(state.mode);
return seq[0];
}
const seq = getStepSequence(state.mode);
const idx = seq.indexOf(state.currentStep);
if (idx === -1 || idx >= seq.length - 1) return state.currentStep;
return seq[idx + 1];
}
function goBack(state) {
if (state.currentStep === 'entry') return 'entry';
const seq = getStepSequence(state.mode);
const idx = seq.indexOf(state.currentStep);
if (idx <= 0) return 'entry';
return seq[idx - 1];
}
// ---- Whether the current step is the last one before creation --------------
function isLastStep(mode, step) {
return step === 'review' || (mode === 'quick' && step === 'basics');
}
// ============================================================================
// Tests
// ============================================================================
describe('WizardContext: step sequences', () => {
it('quick mode only visits basics', () => {
expect(getStepSequence('quick')).toEqual(['basics']);
});
it('guided mode visits basics, preset, review', () => {
expect(getStepSequence('guided')).toEqual(['basics', 'preset', 'review']);
});
it('ai mode uses same sequence as guided (V1 stub)', () => {
expect(getStepSequence('ai')).toEqual(['basics', 'preset', 'review']);
});
});
describe('WizardContext: validation', () => {
const baseState = {
mode: 'quick',
currentStep: 'basics',
projectName: '',
description: '',
location: '',
selectedPresetId: 'modern'
};
it('entry step is always valid', () => {
expect(isStepValid('entry', { ...baseState, currentStep: 'entry' })).toBe(true);
});
it('review step is always valid', () => {
expect(isStepValid('review', { ...baseState, currentStep: 'review' })).toBe(true);
});
it('basics step requires projectName and location', () => {
expect(isStepValid('basics', baseState)).toBe(false);
});
it('basics step passes with name and location', () => {
expect(isStepValid('basics', { ...baseState, projectName: 'My Project', location: '/tmp' })).toBe(true);
});
it('basics step trims whitespace on projectName', () => {
expect(isStepValid('basics', { ...baseState, projectName: ' ', location: '/tmp' })).toBe(false);
});
it('preset step requires selectedPresetId', () => {
expect(isStepValid('preset', { ...baseState, selectedPresetId: '' })).toBe(false);
});
it('preset step passes with a preset id', () => {
expect(isStepValid('preset', { ...baseState, selectedPresetId: 'minimal' })).toBe(true);
});
});
describe('WizardContext: goNext navigation', () => {
const baseState = {
mode: 'quick',
currentStep: 'entry',
projectName: 'Test',
description: '',
location: '/tmp',
selectedPresetId: 'modern'
};
it('quick: entry advances to basics', () => {
expect(goNext({ ...baseState, mode: 'quick', currentStep: 'entry' })).toBe('basics');
});
it('quick: basics stays (is the last step)', () => {
expect(goNext({ ...baseState, mode: 'quick', currentStep: 'basics' })).toBe('basics');
});
it('guided: entry advances to basics', () => {
expect(goNext({ ...baseState, mode: 'guided', currentStep: 'entry' })).toBe('basics');
});
it('guided: basics advances to preset', () => {
expect(goNext({ ...baseState, mode: 'guided', currentStep: 'basics' })).toBe('preset');
});
it('guided: preset advances to review', () => {
expect(goNext({ ...baseState, mode: 'guided', currentStep: 'preset' })).toBe('review');
});
it('guided: review stays (is the last step)', () => {
expect(goNext({ ...baseState, mode: 'guided', currentStep: 'review' })).toBe('review');
});
});
describe('WizardContext: goBack navigation', () => {
const baseState = {
mode: 'guided',
currentStep: 'review',
projectName: 'Test',
description: '',
location: '/tmp',
selectedPresetId: 'modern'
};
it('entry stays on entry when going back', () => {
expect(goBack({ ...baseState, currentStep: 'entry' })).toBe('entry');
});
it('guided: basics goes back to entry', () => {
expect(goBack({ ...baseState, currentStep: 'basics' })).toBe('entry');
});
it('guided: preset goes back to basics', () => {
expect(goBack({ ...baseState, currentStep: 'preset' })).toBe('basics');
});
it('guided: review goes back to preset', () => {
expect(goBack({ ...baseState, currentStep: 'review' })).toBe('preset');
});
it('quick: basics goes back to entry', () => {
expect(goBack({ ...baseState, mode: 'quick', currentStep: 'basics' })).toBe('entry');
});
});
describe('isLastStep: determines when to show Create Project button', () => {
it('quick mode: basics is the last step', () => {
expect(isLastStep('quick', 'basics')).toBe(true);
});
it('quick mode: entry is not the last step', () => {
expect(isLastStep('quick', 'entry')).toBe(false);
});
it('guided mode: review is the last step', () => {
expect(isLastStep('guided', 'review')).toBe(true);
});
it('guided mode: basics is not the last step', () => {
expect(isLastStep('guided', 'basics')).toBe(false);
});
it('guided mode: preset is not the last step', () => {
expect(isLastStep('guided', 'preset')).toBe(false);
});
});