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:
2026-01-12 20:30:09 +01:00
parent 30c7459984
commit 6c65381fcb
17 changed files with 3033 additions and 1823 deletions

View File

@@ -1,5 +1,5 @@
import { describe, test, expect, vi, beforeEach } from "vitest";
import { loadModules, getModuleById, loadModuleFromUrl, addCustomModule } from "../../src/config/lessons.js";
import { loadModules, getModuleById, loadModuleFromUrl, addCustomModule, loadLearningPaths } from "../../src/config/lessons.js";
describe("Lessons Config Module", () => {
describe("loadModules", () => {
@@ -178,4 +178,86 @@ describe("Lessons Config Module", () => {
expect(result).toBe(false);
});
});
describe("loadLearningPaths", () => {
test("should return an array of learning paths", () => {
const paths = loadLearningPaths();
expect(Array.isArray(paths)).toBe(true);
expect(paths.length).toBeGreaterThanOrEqual(4);
// Check if paths have the right structure
const pathIds = paths.map((p) => p.id);
expect(pathIds).toContain("css-fundamentals");
expect(pathIds).toContain("flexbox-master");
expect(pathIds).toContain("html-forms-expert");
expect(pathIds).toContain("css-animations-pro");
});
test("should validate learning paths on load", () => {
// This should not throw as paths are valid
expect(() => loadLearningPaths()).not.toThrow();
});
test("should resolve module references to actual module objects", () => {
const paths = loadLearningPaths();
paths.forEach((path) => {
expect(Array.isArray(path.modules)).toBe(true);
expect(path.modules.length).toBeGreaterThan(0);
// Check that modules are actual objects, not just IDs
path.modules.forEach((module) => {
expect(typeof module).toBe("object");
expect(module).not.toBeNull();
expect(module.id).toBeDefined();
expect(module.title).toBeDefined();
expect(Array.isArray(module.lessons)).toBe(true);
});
});
});
test("should have required fields on each path", () => {
const paths = loadLearningPaths();
paths.forEach((path) => {
expect(path.id).toBeDefined();
expect(path.title).toBeDefined();
expect(path.goal).toBeDefined();
expect(typeof path.estimatedTime).toBe("number");
expect(path.estimatedTime).toBeGreaterThan(0);
expect(["beginner", "intermediate", "advanced"]).toContain(path.difficulty);
expect(Array.isArray(path.modules)).toBe(true);
expect(path.modules.length).toBeGreaterThan(0);
});
});
test("should support different languages", () => {
const pathsEN = loadLearningPaths("en");
const pathsDE = loadLearningPaths("de");
expect(Array.isArray(pathsEN)).toBe(true);
expect(Array.isArray(pathsDE)).toBe(true);
// Both should have the same number of paths (structure is the same)
expect(pathsEN.length).toBe(pathsDE.length);
// Modules should be resolved for each language
pathsEN.forEach((path) => {
expect(path.modules.length).toBeGreaterThan(0);
});
pathsDE.forEach((path) => {
expect(path.modules.length).toBeGreaterThan(0);
});
});
test("should handle missing modules gracefully", () => {
const paths = loadLearningPaths();
// Should not throw even if some module references can't be resolved
// (they are filtered out with a console warning)
expect(Array.isArray(paths)).toBe(true);
});
});
});