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:
706
tests/unit/pathIntegration.test.js
Normal file
706
tests/unit/pathIntegration.test.js
Normal file
@@ -0,0 +1,706 @@
|
||||
/**
|
||||
* Integration tests for LessonEngine + PathManager
|
||||
* Tests: path navigation across modules, progress sync, pause/resume, switching paths
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { LessonEngine } from "../../src/impl/LessonEngine.js";
|
||||
import { PathManager } from "../../src/impl/PathManager.js";
|
||||
|
||||
describe("PathManager + LessonEngine Integration", () => {
|
||||
let lessonEngine;
|
||||
let pathManager;
|
||||
let mockModules;
|
||||
let mockPaths;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
|
||||
// Create comprehensive mock modules
|
||||
mockModules = [
|
||||
{
|
||||
id: "css-basics",
|
||||
title: "CSS Basics",
|
||||
mode: "css",
|
||||
lessons: [
|
||||
{
|
||||
id: "selectors-1",
|
||||
title: "Basic Selectors",
|
||||
task: "Style with color: steelblue",
|
||||
initialCode: "",
|
||||
codePrefix: ".box {\n ",
|
||||
codeSuffix: "\n}",
|
||||
previewHTML: '<div class="box">Hello</div>',
|
||||
previewBaseCSS: "",
|
||||
validations: [{ type: "contains", value: "color: steelblue" }]
|
||||
},
|
||||
{
|
||||
id: "selectors-2",
|
||||
title: "Class Selectors",
|
||||
task: "Style with color: coral",
|
||||
initialCode: "",
|
||||
codePrefix: ".card {\n ",
|
||||
codeSuffix: "\n}",
|
||||
previewHTML: '<div class="card">Card</div>',
|
||||
previewBaseCSS: "",
|
||||
validations: [{ type: "contains", value: "color: coral" }]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "flexbox-intro",
|
||||
title: "Flexbox Introduction",
|
||||
mode: "css",
|
||||
lessons: [
|
||||
{
|
||||
id: "flex-1",
|
||||
title: "Display Flex",
|
||||
task: "Set display: flex",
|
||||
initialCode: "",
|
||||
codePrefix: ".wrap {\n ",
|
||||
codeSuffix: "\n}",
|
||||
previewHTML: '<div class="wrap"><div>1</div><div>2</div></div>',
|
||||
previewBaseCSS: "",
|
||||
validations: [{ type: "property_value", property: "display", expected: "flex" }]
|
||||
},
|
||||
{
|
||||
id: "flex-2",
|
||||
title: "Justify Content",
|
||||
task: "Center items",
|
||||
initialCode: "",
|
||||
codePrefix: ".wrap {\n ",
|
||||
codeSuffix: "\n}",
|
||||
previewHTML: '<div class="wrap"><div>1</div><div>2</div></div>',
|
||||
previewBaseCSS: "",
|
||||
validations: [{ type: "contains", value: "justify-content: center" }]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "grid-basics",
|
||||
title: "CSS Grid Basics",
|
||||
mode: "css",
|
||||
lessons: [
|
||||
{
|
||||
id: "grid-1",
|
||||
title: "Display Grid",
|
||||
task: "Set display: grid",
|
||||
initialCode: "",
|
||||
codePrefix: ".grid {\n ",
|
||||
codeSuffix: "\n}",
|
||||
previewHTML: '<div class="grid"><div>A</div><div>B</div></div>',
|
||||
previewBaseCSS: "",
|
||||
validations: [{ type: "property_value", property: "display", expected: "grid" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// Create mock learning paths
|
||||
mockPaths = [
|
||||
{
|
||||
id: "css-fundamentals",
|
||||
title: "CSS Fundamentals",
|
||||
goal: "Master CSS basics",
|
||||
difficulty: "beginner",
|
||||
estimatedTime: 60,
|
||||
modules: [mockModules[0], mockModules[1]] // css-basics + flexbox-intro
|
||||
},
|
||||
{
|
||||
id: "layout-master",
|
||||
title: "Layout Master",
|
||||
goal: "Master layouts",
|
||||
difficulty: "intermediate",
|
||||
estimatedTime: 90,
|
||||
modules: [mockModules[1], mockModules[2]] // flexbox-intro + grid-basics
|
||||
},
|
||||
{
|
||||
id: "complete-path",
|
||||
title: "Complete Journey",
|
||||
goal: "Learn everything",
|
||||
difficulty: "advanced",
|
||||
estimatedTime: 120,
|
||||
modules: mockModules // All modules
|
||||
}
|
||||
];
|
||||
|
||||
// Initialize
|
||||
lessonEngine = new LessonEngine();
|
||||
lessonEngine.setModules(mockModules);
|
||||
|
||||
pathManager = new PathManager();
|
||||
pathManager.setPaths(mockPaths);
|
||||
|
||||
// Connect integration
|
||||
lessonEngine.setPathManager(pathManager);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
describe("Path Navigation Across Modules", () => {
|
||||
it("should navigate through lessons in multiple modules following path order", () => {
|
||||
pathManager.startPath("css-fundamentals");
|
||||
|
||||
// Start at first lesson
|
||||
lessonEngine.setModuleById("css-basics");
|
||||
lessonEngine.setLessonByIndex(0);
|
||||
|
||||
expect(lessonEngine.currentModule.id).toBe("css-basics");
|
||||
expect(lessonEngine.currentLessonIndex).toBe(0);
|
||||
|
||||
// Complete lesson 1
|
||||
lessonEngine.applyUserCode("color: steelblue");
|
||||
lessonEngine.validateCode();
|
||||
|
||||
// Navigate to next lesson
|
||||
lessonEngine.nextLesson();
|
||||
expect(lessonEngine.currentModule.id).toBe("css-basics");
|
||||
expect(lessonEngine.currentLessonIndex).toBe(1);
|
||||
|
||||
// Complete lesson 2
|
||||
lessonEngine.applyUserCode("color: coral");
|
||||
lessonEngine.validateCode();
|
||||
|
||||
// Should cross to next module in path
|
||||
lessonEngine.nextLesson();
|
||||
expect(lessonEngine.currentModule.id).toBe("flexbox-intro");
|
||||
expect(lessonEngine.currentLessonIndex).toBe(0);
|
||||
});
|
||||
|
||||
it("should skip modules not in the active path", () => {
|
||||
// layout-master path includes flexbox-intro and grid-basics (skips css-basics)
|
||||
pathManager.startPath("layout-master");
|
||||
|
||||
// Navigate from flexbox-intro to grid-basics
|
||||
lessonEngine.setModuleById("flexbox-intro");
|
||||
lessonEngine.setLessonByIndex(0);
|
||||
|
||||
// Complete flexbox lessons
|
||||
lessonEngine.applyUserCode("display: flex");
|
||||
lessonEngine.validateCode();
|
||||
lessonEngine.nextLesson();
|
||||
|
||||
lessonEngine.applyUserCode("justify-content: center");
|
||||
lessonEngine.validateCode();
|
||||
|
||||
// Next should go to grid-basics (skipping css-basics which isn't in path)
|
||||
const result = lessonEngine.nextLesson();
|
||||
expect(result).toBe(true);
|
||||
expect(lessonEngine.currentModule.id).toBe("grid-basics");
|
||||
expect(lessonEngine.currentLessonIndex).toBe(0);
|
||||
});
|
||||
|
||||
it("should handle reaching end of path", () => {
|
||||
pathManager.startPath("css-fundamentals");
|
||||
|
||||
// Navigate to last module and lesson
|
||||
lessonEngine.setModuleById("flexbox-intro");
|
||||
lessonEngine.setLessonByIndex(1);
|
||||
|
||||
// Mark all as complete
|
||||
pathManager.markLessonCompleted("css-basics", 0);
|
||||
pathManager.markLessonCompleted("css-basics", 1);
|
||||
pathManager.markLessonCompleted("flexbox-intro", 0);
|
||||
pathManager.markLessonCompleted("flexbox-intro", 1);
|
||||
|
||||
// Should return false as path is complete
|
||||
const result = lessonEngine.nextLesson();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should find next lesson even when starting mid-path", () => {
|
||||
pathManager.startPath("complete-path");
|
||||
|
||||
// Complete first module's lessons
|
||||
pathManager.markLessonCompleted("css-basics", 0);
|
||||
pathManager.markLessonCompleted("css-basics", 1);
|
||||
|
||||
// Start from second module
|
||||
lessonEngine.setModuleById("flexbox-intro");
|
||||
lessonEngine.setLessonByIndex(0);
|
||||
|
||||
// Should navigate correctly
|
||||
lessonEngine.applyUserCode("display: flex");
|
||||
lessonEngine.validateCode();
|
||||
|
||||
lessonEngine.nextLesson();
|
||||
expect(lessonEngine.currentModule.id).toBe("flexbox-intro");
|
||||
expect(lessonEngine.currentLessonIndex).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Progress Sync Between Path and Module Progress", () => {
|
||||
it("should sync lesson completion to both PathManager and LessonEngine", () => {
|
||||
pathManager.startPath("css-fundamentals");
|
||||
|
||||
lessonEngine.setModuleById("css-basics");
|
||||
lessonEngine.setLessonByIndex(0);
|
||||
|
||||
// Initially not completed in either system
|
||||
expect(lessonEngine.isLessonCompleted("css-basics", 0)).toBe(false);
|
||||
expect(pathManager.isLessonCompleted("css-basics", 0)).toBe(false);
|
||||
|
||||
// Apply valid code and validate
|
||||
lessonEngine.applyUserCode("color: steelblue");
|
||||
const result = lessonEngine.validateCode();
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
|
||||
// Both systems should show completion
|
||||
expect(lessonEngine.isLessonCompleted("css-basics", 0)).toBe(true);
|
||||
expect(pathManager.isLessonCompleted("css-basics", 0)).toBe(true);
|
||||
});
|
||||
|
||||
it("should not mark as complete in PathManager if validation fails", () => {
|
||||
pathManager.startPath("css-fundamentals");
|
||||
|
||||
lessonEngine.setModuleById("css-basics");
|
||||
lessonEngine.setLessonByIndex(0);
|
||||
|
||||
// Apply invalid code
|
||||
lessonEngine.applyUserCode("color: wrong");
|
||||
const result = lessonEngine.validateCode();
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
|
||||
// Neither system should mark as complete
|
||||
expect(lessonEngine.isLessonCompleted("css-basics", 0)).toBe(false);
|
||||
expect(pathManager.isLessonCompleted("css-basics", 0)).toBe(false);
|
||||
});
|
||||
|
||||
it("should track progress independently for different modules", () => {
|
||||
pathManager.startPath("complete-path");
|
||||
|
||||
// Complete lesson in first module
|
||||
lessonEngine.setModuleById("css-basics");
|
||||
lessonEngine.setLessonByIndex(0);
|
||||
lessonEngine.applyUserCode("color: steelblue");
|
||||
lessonEngine.validateCode();
|
||||
|
||||
// Complete lesson in second module
|
||||
lessonEngine.setModuleById("flexbox-intro");
|
||||
lessonEngine.setLessonByIndex(0);
|
||||
lessonEngine.applyUserCode("display: flex");
|
||||
lessonEngine.validateCode();
|
||||
|
||||
// Both should be tracked independently
|
||||
expect(pathManager.isLessonCompleted("css-basics", 0)).toBe(true);
|
||||
expect(pathManager.isLessonCompleted("flexbox-intro", 0)).toBe(true);
|
||||
expect(pathManager.isLessonCompleted("css-basics", 1)).toBe(false);
|
||||
expect(pathManager.isLessonCompleted("flexbox-intro", 1)).toBe(false);
|
||||
});
|
||||
|
||||
it("should calculate path progress accurately", () => {
|
||||
pathManager.startPath("css-fundamentals");
|
||||
|
||||
// Path has 4 total lessons (2 in css-basics + 2 in flexbox-intro)
|
||||
let progress = pathManager.getPathProgress("css-fundamentals");
|
||||
expect(progress.totalLessons).toBe(4);
|
||||
expect(progress.completedCount).toBe(0);
|
||||
expect(progress.percentComplete).toBe(0);
|
||||
|
||||
// Complete first lesson
|
||||
lessonEngine.setModuleById("css-basics");
|
||||
lessonEngine.setLessonByIndex(0);
|
||||
lessonEngine.applyUserCode("color: steelblue");
|
||||
lessonEngine.validateCode();
|
||||
|
||||
progress = pathManager.getPathProgress("css-fundamentals");
|
||||
expect(progress.completedCount).toBe(1);
|
||||
expect(progress.percentComplete).toBe(25); // 1/4 = 25%
|
||||
|
||||
// Complete second lesson
|
||||
lessonEngine.setLessonByIndex(1);
|
||||
lessonEngine.applyUserCode("color: coral");
|
||||
lessonEngine.validateCode();
|
||||
|
||||
progress = pathManager.getPathProgress("css-fundamentals");
|
||||
expect(progress.completedCount).toBe(2);
|
||||
expect(progress.percentComplete).toBe(50); // 2/4 = 50%
|
||||
});
|
||||
|
||||
it("should not sync to PathManager when no path is active", () => {
|
||||
// No path started
|
||||
lessonEngine.setModuleById("css-basics");
|
||||
lessonEngine.setLessonByIndex(0);
|
||||
|
||||
lessonEngine.applyUserCode("color: steelblue");
|
||||
lessonEngine.validateCode();
|
||||
|
||||
// LessonEngine should track
|
||||
expect(lessonEngine.isLessonCompleted("css-basics", 0)).toBe(true);
|
||||
|
||||
// PathManager should NOT track (no active path)
|
||||
expect(pathManager.isLessonCompleted("css-basics", 0)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Path Pause and Resume", () => {
|
||||
it("should stop following path order after pausing", () => {
|
||||
pathManager.startPath("css-fundamentals");
|
||||
|
||||
lessonEngine.setModuleById("css-basics");
|
||||
lessonEngine.setLessonByIndex(0);
|
||||
|
||||
// Mark first lesson complete
|
||||
pathManager.markLessonCompleted("css-basics", 0);
|
||||
|
||||
// Pause the path
|
||||
pathManager.pausePath();
|
||||
|
||||
// nextLesson() should now follow default module order
|
||||
lessonEngine.nextLesson();
|
||||
|
||||
// Without path, it would go to next lesson in current module
|
||||
expect(lessonEngine.currentModule.id).toBe("css-basics");
|
||||
expect(lessonEngine.currentLessonIndex).toBe(1);
|
||||
});
|
||||
|
||||
it("should resume path order after resuming path", () => {
|
||||
pathManager.startPath("css-fundamentals");
|
||||
|
||||
// Complete first module
|
||||
pathManager.markLessonCompleted("css-basics", 0);
|
||||
pathManager.markLessonCompleted("css-basics", 1);
|
||||
|
||||
lessonEngine.setModuleById("css-basics");
|
||||
lessonEngine.setLessonByIndex(1);
|
||||
|
||||
// Pause path
|
||||
pathManager.pausePath();
|
||||
expect(pathManager.getActivePath()).toBeNull();
|
||||
|
||||
// Resume path
|
||||
pathManager.resumePath("css-fundamentals");
|
||||
expect(pathManager.getActivePath().id).toBe("css-fundamentals");
|
||||
|
||||
// Should follow path order again (next lesson is flexbox-intro)
|
||||
lessonEngine.nextLesson();
|
||||
expect(lessonEngine.currentModule.id).toBe("flexbox-intro");
|
||||
expect(lessonEngine.currentLessonIndex).toBe(0);
|
||||
});
|
||||
|
||||
it("should preserve progress when pausing and resuming", () => {
|
||||
pathManager.startPath("css-fundamentals");
|
||||
|
||||
// Complete some lessons
|
||||
lessonEngine.setModuleById("css-basics");
|
||||
lessonEngine.setLessonByIndex(0);
|
||||
lessonEngine.applyUserCode("color: steelblue");
|
||||
lessonEngine.validateCode();
|
||||
|
||||
lessonEngine.setLessonByIndex(1);
|
||||
lessonEngine.applyUserCode("color: coral");
|
||||
lessonEngine.validateCode();
|
||||
|
||||
let progress = pathManager.getPathProgress("css-fundamentals");
|
||||
expect(progress.completedCount).toBe(2);
|
||||
|
||||
// Pause
|
||||
pathManager.pausePath();
|
||||
|
||||
// Progress should be preserved
|
||||
progress = pathManager.getPathProgress("css-fundamentals");
|
||||
expect(progress.completedCount).toBe(2);
|
||||
|
||||
// Resume
|
||||
pathManager.resumePath("css-fundamentals");
|
||||
|
||||
// Progress still preserved
|
||||
progress = pathManager.getPathProgress("css-fundamentals");
|
||||
expect(progress.completedCount).toBe(2);
|
||||
});
|
||||
|
||||
it("should not sync completion to paused path", () => {
|
||||
pathManager.startPath("css-fundamentals");
|
||||
pathManager.pausePath();
|
||||
|
||||
// Complete a lesson while paused
|
||||
lessonEngine.setModuleById("css-basics");
|
||||
lessonEngine.setLessonByIndex(0);
|
||||
lessonEngine.applyUserCode("color: steelblue");
|
||||
lessonEngine.validateCode();
|
||||
|
||||
// LessonEngine tracks it
|
||||
expect(lessonEngine.isLessonCompleted("css-basics", 0)).toBe(true);
|
||||
|
||||
// PathManager should NOT track (path is paused)
|
||||
expect(pathManager.isLessonCompleted("css-basics", 0)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Switching Between Paths", () => {
|
||||
it("should switch active path and follow new path order", () => {
|
||||
// Start first path
|
||||
pathManager.startPath("css-fundamentals");
|
||||
expect(pathManager.getActivePath().id).toBe("css-fundamentals");
|
||||
|
||||
// Complete a lesson
|
||||
lessonEngine.setModuleById("css-basics");
|
||||
lessonEngine.setLessonByIndex(0);
|
||||
lessonEngine.applyUserCode("color: steelblue");
|
||||
lessonEngine.validateCode();
|
||||
|
||||
// Switch to different path
|
||||
pathManager.startPath("layout-master");
|
||||
expect(pathManager.getActivePath().id).toBe("layout-master");
|
||||
|
||||
// Navigate from layout-master start
|
||||
lessonEngine.setModuleById("flexbox-intro");
|
||||
lessonEngine.setLessonByIndex(0);
|
||||
|
||||
// Complete lessons in flexbox module
|
||||
pathManager.markLessonCompleted("flexbox-intro", 0);
|
||||
pathManager.markLessonCompleted("flexbox-intro", 1);
|
||||
|
||||
// Should navigate to grid-basics (part of layout-master path)
|
||||
lessonEngine.nextLesson();
|
||||
expect(lessonEngine.currentModule.id).toBe("grid-basics");
|
||||
});
|
||||
|
||||
it("should maintain separate progress for different paths", () => {
|
||||
// Progress in first path
|
||||
pathManager.startPath("css-fundamentals");
|
||||
lessonEngine.setModuleById("css-basics");
|
||||
lessonEngine.setLessonByIndex(0);
|
||||
lessonEngine.applyUserCode("color: steelblue");
|
||||
lessonEngine.validateCode();
|
||||
|
||||
let progress1 = pathManager.getPathProgress("css-fundamentals");
|
||||
expect(progress1.completedCount).toBe(1);
|
||||
|
||||
// Switch to second path
|
||||
pathManager.startPath("layout-master");
|
||||
lessonEngine.setModuleById("flexbox-intro");
|
||||
lessonEngine.setLessonByIndex(0);
|
||||
lessonEngine.applyUserCode("display: flex");
|
||||
lessonEngine.validateCode();
|
||||
|
||||
let progress2 = pathManager.getPathProgress("layout-master");
|
||||
expect(progress2.completedCount).toBe(1);
|
||||
|
||||
// Original path progress should be unchanged
|
||||
progress1 = pathManager.getPathProgress("css-fundamentals");
|
||||
expect(progress1.completedCount).toBe(1);
|
||||
});
|
||||
|
||||
it("should allow resuming previously started path", () => {
|
||||
// Start and make progress in path 1
|
||||
pathManager.startPath("css-fundamentals");
|
||||
pathManager.markLessonCompleted("css-basics", 0);
|
||||
|
||||
// Switch to path 2
|
||||
pathManager.startPath("layout-master");
|
||||
pathManager.markLessonCompleted("flexbox-intro", 0);
|
||||
|
||||
// Resume path 1
|
||||
pathManager.resumePath("css-fundamentals");
|
||||
expect(pathManager.getActivePath().id).toBe("css-fundamentals");
|
||||
|
||||
// Progress should be preserved
|
||||
const progress = pathManager.getPathProgress("css-fundamentals");
|
||||
expect(progress.completedCount).toBe(1);
|
||||
|
||||
// Should continue from where left off
|
||||
const nextLesson = pathManager.getNextLesson("css-fundamentals");
|
||||
expect(nextLesson).toEqual({ moduleId: "css-basics", lessonIndex: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("LocalStorage Persistence", () => {
|
||||
it("should persist path progress across sessions", () => {
|
||||
pathManager.startPath("css-fundamentals");
|
||||
|
||||
lessonEngine.setModuleById("css-basics");
|
||||
lessonEngine.setLessonByIndex(0);
|
||||
lessonEngine.applyUserCode("color: steelblue");
|
||||
lessonEngine.validateCode();
|
||||
|
||||
// Simulate new session
|
||||
const newLessonEngine = new LessonEngine();
|
||||
newLessonEngine.setModules(mockModules);
|
||||
|
||||
const newPathManager = new PathManager();
|
||||
newPathManager.setPaths(mockPaths);
|
||||
|
||||
newLessonEngine.setPathManager(newPathManager);
|
||||
|
||||
// Check persisted state
|
||||
expect(newPathManager.getActivePath().id).toBe("css-fundamentals");
|
||||
|
||||
const progress = newPathManager.getPathProgress("css-fundamentals");
|
||||
expect(progress.completedCount).toBe(1);
|
||||
expect(newPathManager.isLessonCompleted("css-basics", 0)).toBe(true);
|
||||
});
|
||||
|
||||
it("should persist module progress across sessions", () => {
|
||||
lessonEngine.setModuleById("css-basics");
|
||||
lessonEngine.setLessonByIndex(0);
|
||||
lessonEngine.applyUserCode("color: steelblue");
|
||||
lessonEngine.validateCode();
|
||||
|
||||
// Simulate new session
|
||||
const newEngine = new LessonEngine();
|
||||
newEngine.setModules(mockModules);
|
||||
|
||||
// Module progress should be loaded
|
||||
expect(newEngine.isLessonCompleted("css-basics", 0)).toBe(true);
|
||||
});
|
||||
|
||||
it("should persist both systems independently", () => {
|
||||
// Complete lesson with path active
|
||||
pathManager.startPath("css-fundamentals");
|
||||
lessonEngine.setModuleById("css-basics");
|
||||
lessonEngine.setLessonByIndex(0);
|
||||
lessonEngine.applyUserCode("color: steelblue");
|
||||
lessonEngine.validateCode();
|
||||
|
||||
// Complete another lesson without path
|
||||
pathManager.pausePath();
|
||||
lessonEngine.setModuleById("flexbox-intro");
|
||||
lessonEngine.setLessonByIndex(0);
|
||||
lessonEngine.applyUserCode("display: flex");
|
||||
lessonEngine.validateCode();
|
||||
|
||||
// Simulate new session
|
||||
const newEngine = new LessonEngine();
|
||||
newEngine.setModules(mockModules);
|
||||
|
||||
const newPathManager = new PathManager();
|
||||
newPathManager.setPaths(mockPaths);
|
||||
|
||||
newEngine.setPathManager(newPathManager);
|
||||
|
||||
// Both should be persisted correctly
|
||||
expect(newEngine.isLessonCompleted("css-basics", 0)).toBe(true);
|
||||
expect(newEngine.isLessonCompleted("flexbox-intro", 0)).toBe(true);
|
||||
expect(newPathManager.isLessonCompleted("css-basics", 0)).toBe(true);
|
||||
expect(newPathManager.isLessonCompleted("flexbox-intro", 0)).toBe(false); // Wasn't completed during active path
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge Cases and Error Handling", () => {
|
||||
it("should handle missing PathManager gracefully", () => {
|
||||
const engine = new LessonEngine();
|
||||
engine.setModules(mockModules);
|
||||
|
||||
// No PathManager set
|
||||
expect(engine.pathManager).toBeNull();
|
||||
|
||||
// Should navigate normally
|
||||
engine.setModuleById("css-basics");
|
||||
engine.setLessonByIndex(0);
|
||||
|
||||
engine.nextLesson();
|
||||
expect(engine.currentModule.id).toBe("css-basics");
|
||||
expect(engine.currentLessonIndex).toBe(1);
|
||||
});
|
||||
|
||||
it("should handle getNextLesson returning null gracefully", () => {
|
||||
pathManager.startPath("css-fundamentals");
|
||||
|
||||
// Complete all lessons
|
||||
pathManager.markLessonCompleted("css-basics", 0);
|
||||
pathManager.markLessonCompleted("css-basics", 1);
|
||||
pathManager.markLessonCompleted("flexbox-intro", 0);
|
||||
pathManager.markLessonCompleted("flexbox-intro", 1);
|
||||
|
||||
lessonEngine.setModuleById("flexbox-intro");
|
||||
lessonEngine.setLessonByIndex(1);
|
||||
|
||||
// Should return false when path complete
|
||||
const result = lessonEngine.nextLesson();
|
||||
expect(result).toBe(false);
|
||||
|
||||
// Current position should remain unchanged
|
||||
expect(lessonEngine.currentModule.id).toBe("flexbox-intro");
|
||||
expect(lessonEngine.currentLessonIndex).toBe(1);
|
||||
});
|
||||
|
||||
it("should handle invalid module ID in path gracefully", () => {
|
||||
const invalidPath = {
|
||||
id: "invalid-path",
|
||||
title: "Invalid Path",
|
||||
goal: "Test",
|
||||
estimatedTime: 30,
|
||||
difficulty: "beginner",
|
||||
modules: [{ id: "nonexistent-module", lessons: [{}] }]
|
||||
};
|
||||
|
||||
pathManager.setPaths([...mockPaths, invalidPath]);
|
||||
pathManager.startPath("invalid-path");
|
||||
|
||||
// Try to navigate
|
||||
const nextLesson = pathManager.getNextLesson("invalid-path");
|
||||
expect(nextLesson).toEqual({ moduleId: "nonexistent-module", lessonIndex: 0 });
|
||||
|
||||
// setModuleById should return false for invalid module
|
||||
const result = lessonEngine.setModuleById("nonexistent-module");
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle completing lessons in different order", () => {
|
||||
pathManager.startPath("css-fundamentals");
|
||||
|
||||
// Complete lessons out of order
|
||||
lessonEngine.setModuleById("css-basics");
|
||||
lessonEngine.setLessonByIndex(1);
|
||||
lessonEngine.applyUserCode("color: coral");
|
||||
lessonEngine.validateCode();
|
||||
|
||||
// First lesson should still be incomplete
|
||||
expect(pathManager.isLessonCompleted("css-basics", 0)).toBe(false);
|
||||
expect(pathManager.isLessonCompleted("css-basics", 1)).toBe(true);
|
||||
|
||||
// getNextLesson should return first incomplete (lesson 0)
|
||||
const nextLesson = pathManager.getNextLesson("css-fundamentals");
|
||||
expect(nextLesson).toEqual({ moduleId: "css-basics", lessonIndex: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Path Completion Detection", () => {
|
||||
it("should detect when path is complete", () => {
|
||||
pathManager.startPath("layout-master");
|
||||
|
||||
// layout-master has 3 lessons total
|
||||
expect(pathManager.isPathComplete("layout-master")).toBe(false);
|
||||
|
||||
// Complete all lessons
|
||||
lessonEngine.setModuleById("flexbox-intro");
|
||||
lessonEngine.setLessonByIndex(0);
|
||||
lessonEngine.applyUserCode("display: flex");
|
||||
lessonEngine.validateCode();
|
||||
|
||||
lessonEngine.setLessonByIndex(1);
|
||||
lessonEngine.applyUserCode("justify-content: center");
|
||||
lessonEngine.validateCode();
|
||||
|
||||
lessonEngine.setModuleById("grid-basics");
|
||||
lessonEngine.setLessonByIndex(0);
|
||||
lessonEngine.applyUserCode("display: grid");
|
||||
lessonEngine.validateCode();
|
||||
|
||||
// Path should be complete
|
||||
expect(pathManager.isPathComplete("layout-master")).toBe(true);
|
||||
|
||||
// Progress should show 100%
|
||||
const progress = pathManager.getPathProgress("layout-master");
|
||||
expect(progress.percentComplete).toBe(100);
|
||||
expect(progress.isComplete).toBe(true);
|
||||
});
|
||||
|
||||
it("should return null for next lesson when path is complete", () => {
|
||||
pathManager.startPath("layout-master");
|
||||
|
||||
// Complete all lessons
|
||||
pathManager.markLessonCompleted("flexbox-intro", 0);
|
||||
pathManager.markLessonCompleted("flexbox-intro", 1);
|
||||
pathManager.markLessonCompleted("grid-basics", 0);
|
||||
|
||||
// No next lesson
|
||||
const nextLesson = pathManager.getNextLesson("layout-master");
|
||||
expect(nextLesson).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user