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
This commit is contained in:
322
tests/unit/lessonEngine-pathManager-integration.test.js
Normal file
322
tests/unit/lessonEngine-pathManager-integration.test.js
Normal file
@@ -0,0 +1,322 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user