mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-03-07 17:43:28 +01:00
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.
216 lines
6.4 KiB
TypeScript
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);
|
|
});
|
|
});
|