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
323 lines
9.2 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|