Files
code-crispies/tests/unit/lessonEngine-pathManager-integration.test.js
Michael Czechowski 6c65381fcb feat: add Guided Learning Paths feature
Implement PathManager to orchestrate multi-module learning journeys:
- Add PathManager class with start/pause/resume functionality
- Create learning-paths.json config with CSS Fundamentals path
- Integrate path progress tracking with LessonEngine
- Add path selection UI to homepage and navigation
- Include JSON schema for learning path validation
- Add comprehensive test suite for PathManager
2026-01-12 20:30:09 +01:00

323 lines
9.2 KiB
JavaScript

import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { LessonEngine } from "../../src/impl/LessonEngine.js";
import { PathManager } from "../../src/impl/PathManager.js";
describe("LessonEngine + PathManager Integration", () => {
let lessonEngine;
let pathManager;
let mockModules;
let mockPaths;
beforeEach(() => {
// Clear localStorage before each test
localStorage.clear();
// Create mock modules
mockModules = [
{
id: "module-1",
title: "Module 1",
mode: "css",
lessons: [
{
id: "lesson-1-1",
title: "Lesson 1.1",
task: "Test task 1",
initialCode: "",
validations: [{ type: "contains", value: "color: red" }]
},
{
id: "lesson-1-2",
title: "Lesson 1.2",
task: "Test task 2",
initialCode: "",
validations: [{ type: "contains", value: "color: blue" }]
}
]
},
{
id: "module-2",
title: "Module 2",
mode: "css",
lessons: [
{
id: "lesson-2-1",
title: "Lesson 2.1",
task: "Test task 3",
initialCode: "",
validations: [{ type: "contains", value: "color: green" }]
},
{
id: "lesson-2-2",
title: "Lesson 2.2",
task: "Test task 4",
initialCode: "",
validations: [{ type: "contains", value: "color: yellow" }]
}
]
},
{
id: "module-3",
title: "Module 3",
mode: "css",
lessons: [
{
id: "lesson-3-1",
title: "Lesson 3.1",
task: "Test task 5",
initialCode: "",
validations: [{ type: "contains", value: "color: orange" }]
}
]
}
];
// Create mock paths
mockPaths = [
{
id: "path-1",
title: "Test Path 1",
goal: "Learn basics",
estimatedTime: 60,
difficulty: "beginner",
modules: mockModules // Path includes all modules
},
{
id: "path-2",
title: "Test Path 2",
goal: "Advanced concepts",
estimatedTime: 90,
difficulty: "intermediate",
modules: [mockModules[1], mockModules[2]] // Only modules 2 and 3
}
];
// Initialize LessonEngine and PathManager
lessonEngine = new LessonEngine();
lessonEngine.setModules(mockModules);
pathManager = new PathManager();
pathManager.setPaths(mockPaths);
// Connect PathManager to LessonEngine
lessonEngine.setPathManager(pathManager);
});
afterEach(() => {
localStorage.clear();
});
describe("setPathManager()", () => {
it("should set the PathManager instance", () => {
const engine = new LessonEngine();
const pm = new PathManager();
expect(engine.pathManager).toBeNull();
engine.setPathManager(pm);
expect(engine.pathManager).toBe(pm);
});
});
describe("nextLesson() with no active path", () => {
it("should follow normal module order when no path is active", () => {
lessonEngine.setModule(mockModules[0]);
lessonEngine.setLessonByIndex(0);
expect(lessonEngine.currentLessonIndex).toBe(0);
expect(lessonEngine.currentModule.id).toBe("module-1");
// Move to next lesson in same module
lessonEngine.nextLesson();
expect(lessonEngine.currentLessonIndex).toBe(1);
expect(lessonEngine.currentModule.id).toBe("module-1");
// Move to next module's first lesson
lessonEngine.nextLesson();
expect(lessonEngine.currentLessonIndex).toBe(0);
expect(lessonEngine.currentModule.id).toBe("module-2");
});
});
describe("nextLesson() with active path", () => {
it("should follow path order when path is active", () => {
// Start path-2 which includes only module-2 and module-3
pathManager.startPath("path-2");
// Start at first lesson of path (module-2, lesson 0)
const firstLesson = pathManager.getNextLesson("path-2");
expect(firstLesson).toEqual({ moduleId: "module-2", lessonIndex: 0 });
lessonEngine.setModuleById(firstLesson.moduleId);
lessonEngine.setLessonByIndex(firstLesson.lessonIndex);
expect(lessonEngine.currentModule.id).toBe("module-2");
expect(lessonEngine.currentLessonIndex).toBe(0);
// Mark first lesson complete
pathManager.markLessonCompleted("module-2", 0);
// Next lesson should be module-2, lesson 1
lessonEngine.nextLesson();
expect(lessonEngine.currentModule.id).toBe("module-2");
expect(lessonEngine.currentLessonIndex).toBe(1);
});
it("should navigate across modules within the path", () => {
pathManager.startPath("path-2");
// Start at module-2, lesson 0
lessonEngine.setModuleById("module-2");
lessonEngine.setLessonByIndex(0);
// Complete both lessons in module-2
pathManager.markLessonCompleted("module-2", 0);
pathManager.markLessonCompleted("module-2", 1);
// Next lesson should jump to module-3 (skipping module-1 which isn't in path)
lessonEngine.nextLesson();
expect(lessonEngine.currentModule.id).toBe("module-3");
expect(lessonEngine.currentLessonIndex).toBe(0);
});
it("should return false when path is complete", () => {
pathManager.startPath("path-2");
// Complete all lessons in path-2
pathManager.markLessonCompleted("module-2", 0);
pathManager.markLessonCompleted("module-2", 1);
pathManager.markLessonCompleted("module-3", 0);
// Set to last lesson
lessonEngine.setModuleById("module-3");
lessonEngine.setLessonByIndex(0);
// Should return false as there's no next lesson in the path
const result = lessonEngine.nextLesson();
expect(result).toBe(false);
});
});
describe("validateCode() with active path", () => {
it("should mark lesson complete in both LessonEngine and PathManager", () => {
pathManager.startPath("path-1");
lessonEngine.setModule(mockModules[0]);
lessonEngine.setLessonByIndex(0);
// Lesson should not be completed initially
expect(lessonEngine.isCurrentLessonCompleted()).toBe(false);
expect(pathManager.isLessonCompleted("module-1", 0)).toBe(false);
// Apply valid code
lessonEngine.applyUserCode("color: red");
const result = lessonEngine.validateCode();
expect(result.isValid).toBe(true);
// Both should mark it as completed
expect(lessonEngine.isCurrentLessonCompleted()).toBe(true);
expect(pathManager.isLessonCompleted("module-1", 0)).toBe(true);
});
it("should not mark lesson complete in PathManager if validation fails", () => {
pathManager.startPath("path-1");
lessonEngine.setModule(mockModules[0]);
lessonEngine.setLessonByIndex(0);
// Apply invalid code
lessonEngine.applyUserCode("color: wrong");
const result = lessonEngine.validateCode();
expect(result.isValid).toBe(false);
// Neither should mark it as completed
expect(lessonEngine.isCurrentLessonCompleted()).toBe(false);
expect(pathManager.isLessonCompleted("module-1", 0)).toBe(false);
});
it("should work normally without active path", () => {
// No path started
lessonEngine.setModule(mockModules[0]);
lessonEngine.setLessonByIndex(0);
lessonEngine.applyUserCode("color: red");
const result = lessonEngine.validateCode();
expect(result.isValid).toBe(true);
// Only LessonEngine should track completion
expect(lessonEngine.isCurrentLessonCompleted()).toBe(true);
// PathManager doesn't track without active path
expect(pathManager.isLessonCompleted("module-1", 0)).toBe(false);
});
});
describe("Path-aware navigation workflow", () => {
it("should guide user through complete path workflow", () => {
// Start a path
pathManager.startPath("path-2");
expect(pathManager.getActivePath().id).toBe("path-2");
// Get first lesson and navigate to it
const firstLesson = pathManager.getNextLesson("path-2");
lessonEngine.setModuleById(firstLesson.moduleId);
lessonEngine.setLessonByIndex(firstLesson.lessonIndex);
expect(lessonEngine.currentModule.id).toBe("module-2");
expect(lessonEngine.currentLessonIndex).toBe(0);
// Complete first lesson
lessonEngine.applyUserCode("color: green");
lessonEngine.validateCode();
// Navigate to next lesson using path order
lessonEngine.nextLesson();
expect(lessonEngine.currentModule.id).toBe("module-2");
expect(lessonEngine.currentLessonIndex).toBe(1);
// Complete second lesson
lessonEngine.applyUserCode("color: yellow");
lessonEngine.validateCode();
// Navigate to next lesson (should cross to module-3)
lessonEngine.nextLesson();
expect(lessonEngine.currentModule.id).toBe("module-3");
expect(lessonEngine.currentLessonIndex).toBe(0);
// Complete final lesson
lessonEngine.applyUserCode("color: orange");
lessonEngine.validateCode();
// Check path completion
expect(pathManager.isPathComplete("path-2")).toBe(true);
// No more lessons
const result = lessonEngine.nextLesson();
expect(result).toBe(false);
});
});
describe("PathManager integration without setting PathManager", () => {
it("should work normally when PathManager is not set", () => {
const engine = new LessonEngine();
engine.setModules(mockModules);
expect(engine.pathManager).toBeNull();
// Should navigate normally
engine.setModule(mockModules[0]);
engine.setLessonByIndex(0);
engine.nextLesson();
expect(engine.currentModule.id).toBe("module-1");
expect(engine.currentLessonIndex).toBe(1);
// Validation should work
engine.applyUserCode("color: blue");
const result = engine.validateCode();
expect(result.isValid).toBe(true);
});
});
});