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:
197
tests/unit/pathManager-start-pause-resume.test.js
Normal file
197
tests/unit/pathManager-start-pause-resume.test.js
Normal file
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* Tests for PathManager start/pause/resume functionality
|
||||
*/
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { PathManager } from "../../src/impl/PathManager.js";
|
||||
|
||||
describe("PathManager - Start/Pause/Resume/GetActivePath", () => {
|
||||
let pathManager;
|
||||
let mockPaths;
|
||||
|
||||
beforeEach(() => {
|
||||
// Clear localStorage before each test
|
||||
localStorage.clear();
|
||||
|
||||
// Create mock paths
|
||||
mockPaths = [
|
||||
{
|
||||
id: "css-fundamentals",
|
||||
title: "CSS Fundamentals",
|
||||
modules: [
|
||||
{ id: "basic-selectors", lessons: [{}, {}, {}] }
|
||||
],
|
||||
estimatedTime: 60
|
||||
},
|
||||
{
|
||||
id: "flexbox-master",
|
||||
title: "Flexbox Master",
|
||||
modules: [
|
||||
{ id: "flex-basics", lessons: [{}, {}] }
|
||||
],
|
||||
estimatedTime: 90
|
||||
}
|
||||
];
|
||||
|
||||
// Create fresh PathManager instance
|
||||
pathManager = new PathManager();
|
||||
pathManager.setPaths(mockPaths);
|
||||
});
|
||||
|
||||
describe("getActivePath()", () => {
|
||||
it("should return null when no path is active", () => {
|
||||
const result = pathManager.getActivePath();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return the active path object after starting a path", () => {
|
||||
pathManager.startPath("css-fundamentals");
|
||||
const result = pathManager.getActivePath();
|
||||
expect(result).not.toBeNull();
|
||||
expect(result.id).toBe("css-fundamentals");
|
||||
expect(result.title).toBe("CSS Fundamentals");
|
||||
});
|
||||
|
||||
it("should return null after pausing", () => {
|
||||
pathManager.startPath("css-fundamentals");
|
||||
pathManager.pausePath();
|
||||
const result = pathManager.getActivePath();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("startPath(pathId)", () => {
|
||||
it("should activate a path and return true", () => {
|
||||
const result = pathManager.startPath("css-fundamentals");
|
||||
expect(result).toBe(true);
|
||||
expect(pathManager.getActivePath()).not.toBeNull();
|
||||
});
|
||||
|
||||
it("should return false for non-existent path", () => {
|
||||
const result = pathManager.startPath("non-existent");
|
||||
expect(result).toBe(false);
|
||||
expect(pathManager.getActivePath()).toBeNull();
|
||||
});
|
||||
|
||||
it("should initialize progress for new path", () => {
|
||||
pathManager.startPath("css-fundamentals");
|
||||
const progress = pathManager.getPathProgress("css-fundamentals");
|
||||
expect(progress.startTimestamp).not.toBeNull();
|
||||
expect(progress.isStarted).toBe(true);
|
||||
});
|
||||
|
||||
it("should switch active path when starting a different path", () => {
|
||||
pathManager.startPath("css-fundamentals");
|
||||
expect(pathManager.getActivePath().id).toBe("css-fundamentals");
|
||||
|
||||
pathManager.startPath("flexbox-master");
|
||||
expect(pathManager.getActivePath().id).toBe("flexbox-master");
|
||||
});
|
||||
|
||||
it("should persist active path to localStorage", () => {
|
||||
pathManager.startPath("css-fundamentals");
|
||||
const saved = JSON.parse(localStorage.getItem("codeCrispies.pathProgress"));
|
||||
expect(saved.activePathId).toBe("css-fundamentals");
|
||||
});
|
||||
});
|
||||
|
||||
describe("pausePath()", () => {
|
||||
it("should deactivate the current path and return true", () => {
|
||||
pathManager.startPath("css-fundamentals");
|
||||
const result = pathManager.pausePath();
|
||||
expect(result).toBe(true);
|
||||
expect(pathManager.getActivePath()).toBeNull();
|
||||
});
|
||||
|
||||
it("should return false when no path is active", () => {
|
||||
const result = pathManager.pausePath();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should update lastActivityTimestamp before pausing", () => {
|
||||
pathManager.startPath("css-fundamentals");
|
||||
const progressBefore = pathManager.getPathProgress("css-fundamentals");
|
||||
const timestampBefore = progressBefore.lastActivityTimestamp;
|
||||
|
||||
// Small delay to ensure timestamp changes
|
||||
const now = new Date().toISOString();
|
||||
pathManager.pausePath();
|
||||
|
||||
const progressAfter = pathManager.getPathProgress("css-fundamentals");
|
||||
expect(progressAfter.lastActivityTimestamp).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should persist inactive state to localStorage", () => {
|
||||
pathManager.startPath("css-fundamentals");
|
||||
pathManager.pausePath();
|
||||
const saved = JSON.parse(localStorage.getItem("codeCrispies.pathProgress"));
|
||||
expect(saved.activePathId).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("resumePath(pathId)", () => {
|
||||
it("should reactivate a previously started path and return true", () => {
|
||||
pathManager.startPath("css-fundamentals");
|
||||
pathManager.pausePath();
|
||||
const result = pathManager.resumePath("css-fundamentals");
|
||||
expect(result).toBe(true);
|
||||
expect(pathManager.getActivePath().id).toBe("css-fundamentals");
|
||||
});
|
||||
|
||||
it("should return false for a path that was never started", () => {
|
||||
const result = pathManager.resumePath("flexbox-master");
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for non-existent path", () => {
|
||||
const result = pathManager.resumePath("non-existent");
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should update lastActivityTimestamp when resuming", () => {
|
||||
pathManager.startPath("css-fundamentals");
|
||||
const timestampBefore = pathManager.getPathProgress("css-fundamentals").lastActivityTimestamp;
|
||||
|
||||
pathManager.pausePath();
|
||||
pathManager.resumePath("css-fundamentals");
|
||||
|
||||
const timestampAfter = pathManager.getPathProgress("css-fundamentals").lastActivityTimestamp;
|
||||
expect(timestampAfter).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should persist resumed state to localStorage", () => {
|
||||
pathManager.startPath("css-fundamentals");
|
||||
pathManager.pausePath();
|
||||
pathManager.resumePath("css-fundamentals");
|
||||
|
||||
const saved = JSON.parse(localStorage.getItem("codeCrispies.pathProgress"));
|
||||
expect(saved.activePathId).toBe("css-fundamentals");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Active path state - Only one path active at a time", () => {
|
||||
it("should only allow one active path at a time", () => {
|
||||
pathManager.startPath("css-fundamentals");
|
||||
expect(pathManager.getActivePath().id).toBe("css-fundamentals");
|
||||
|
||||
pathManager.startPath("flexbox-master");
|
||||
expect(pathManager.getActivePath().id).toBe("flexbox-master");
|
||||
|
||||
// Only flexbox-master should be active
|
||||
const activePath = pathManager.getActivePath();
|
||||
expect(activePath.id).toBe("flexbox-master");
|
||||
});
|
||||
|
||||
it("should store active path state separately from progress", () => {
|
||||
pathManager.startPath("css-fundamentals");
|
||||
const saved = JSON.parse(localStorage.getItem("codeCrispies.pathProgress"));
|
||||
|
||||
// Active path ID stored separately
|
||||
expect(saved).toHaveProperty("activePathId");
|
||||
expect(saved.activePathId).toBe("css-fundamentals");
|
||||
|
||||
// Progress data stored separately
|
||||
expect(saved).toHaveProperty("pathProgress");
|
||||
expect(saved.pathProgress).toHaveProperty("css-fundamentals");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user