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
198 lines
6.3 KiB
JavaScript
198 lines
6.3 KiB
JavaScript
/**
|
|
* 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");
|
|
});
|
|
});
|
|
});
|