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
707 lines
22 KiB
JavaScript
707 lines
22 KiB
JavaScript
/**
|
|
* 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();
|
|
});
|
|
});
|
|
});
|