Files
code-crispies/tests/unit/lessons.test.js
Michael Czechowski 862d29aa19 feat: add HTML lessons mode and side-by-side comparison UI
- Add HTML mode support with new validation types (element_exists,
  element_count, attribute_value, element_text, parent_child, sibling)
- Create 3 HTML lesson modules: Elements, Forms Basic, Forms Validation
- Implement side-by-side preview comparison (Your Output vs Expected)
- Add merge animation with "Perfect Match!" overlay on validation success
- Render expected output from solutionCode field in lesson JSON
- Update schema to support HTML mode and solutionCode
- Reorder modules: HTML first, then CSS, then Tailwind
- Update tests for new functionality

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-21 22:12:00 +01:00

183 lines
5.2 KiB
JavaScript

import { describe, test, expect, vi, beforeEach } from "vitest";
import { loadModules, getModuleById, loadModuleFromUrl, addCustomModule } from "../../src/config/lessons.js";
describe("Lessons Config Module", () => {
describe("loadModules", () => {
test("should return an array of modules", async () => {
const modules = await loadModules();
expect(Array.isArray(modules)).toBe(true);
expect(modules.length).toBe(6);
// Check if modules have the right structure
const moduleIds = modules.map((m) => m.id);
// HTML modules (first)
expect(moduleIds).toContain("html-elements");
expect(moduleIds).toContain("html-forms-basic");
expect(moduleIds).toContain("html-forms-validation");
// CSS modules
expect(moduleIds).toContain("css-basic-selectors");
expect(moduleIds).toContain("css-advanced-selectors");
// Tailwind
expect(moduleIds).toContain("tailwind-basics");
});
test("should have mode set on each lesson", async () => {
const modules = await loadModules();
modules.forEach((module) => {
module.lessons.forEach((lesson) => {
expect(lesson.mode).toBeDefined();
expect(["html", "css", "tailwind"]).toContain(lesson.mode);
});
});
});
});
describe("getModuleById", () => {
test("should return a module by ID", async () => {
// Load modules first to populate the module store
await loadModules();
const htmlModule = getModuleById("html-elements");
expect(htmlModule).not.toBeNull();
expect(htmlModule.id).toBe("html-elements");
expect(htmlModule.mode).toBe("html");
});
test("should return null for non-existent module ID", async () => {
// Load modules first
await loadModules();
const nonExistentModule = getModuleById("non-existent");
expect(nonExistentModule).toBeNull();
});
});
describe("loadModuleFromUrl", () => {
beforeEach(() => {
// Reset fetch mock
fetch.mockReset();
});
test("should load a module from a URL", async () => {
const mockModule = {
id: "remote-module",
title: "Remote Module",
lessons: [{ title: "Lesson 1", previewHTML: "<div>Preview</div>" }]
};
// Mock the fetch response
fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockModule
});
const result = await loadModuleFromUrl("https://example.com/module.json");
expect(fetch).toHaveBeenCalledWith("https://example.com/module.json");
expect(result).toEqual(mockModule);
});
test("should throw an error for failed fetch", async () => {
fetch.mockResolvedValueOnce({
ok: false,
status: 404,
statusText: "Not Found"
});
await expect(loadModuleFromUrl("https://example.com/not-found.json")).rejects.toThrow("Failed to load module: 404 Not Found");
});
test("should validate module structure", async () => {
// Missing required fields
const invalidModule = {
// Missing id
title: "Invalid Module"
// Missing lessons array
};
fetch.mockResolvedValueOnce({
ok: true,
json: async () => invalidModule
});
await expect(loadModuleFromUrl("https://example.com/invalid.json")).rejects.toThrow('Module config missing "id"');
// Invalid lessons structure
const moduleWithInvalidLessons = {
id: "invalid-lessons",
title: "Invalid Lessons",
lessons: [{ /* Missing title */ previewHTML: "<div>Preview</div>" }]
};
fetch.mockResolvedValueOnce({
ok: true,
json: async () => moduleWithInvalidLessons
});
await expect(loadModuleFromUrl("https://example.com/invalid-lessons.json")).rejects.toThrow('Lesson 0 missing "title"');
});
});
describe("addCustomModule", () => {
test("should add a new module to the store", async () => {
// Load modules first to get current count
const initialModules = await loadModules();
const initialCount = initialModules.length;
const customModule = {
id: "custom-module",
title: "Custom Module",
lessons: [{ title: "Custom Lesson", previewHTML: "<div>Preview</div>" }]
};
const result = addCustomModule(customModule);
expect(result).toBe(true);
// Check if module was added
const updatedModules = await loadModules();
expect(updatedModules.length).toBe(initialCount + 1);
const addedModule = getModuleById("custom-module");
expect(addedModule).not.toBeNull();
expect(addedModule.title).toBe("Custom Module");
});
test("should replace existing module with same ID", async () => {
// Add a module first
const customModule = {
id: "replace-test",
title: "Original Module",
lessons: [{ title: "Original Lesson", previewHTML: "<div>Preview</div>" }]
};
addCustomModule(customModule);
// Now replace it
const replacementModule = {
id: "replace-test",
title: "Replacement Module",
lessons: [{ title: "New Lesson", previewHTML: "<div>New Preview</div>" }]
};
const result = addCustomModule(replacementModule);
expect(result).toBe(true);
// Check if module was replaced
const updatedModule = getModuleById("replace-test");
expect(updatedModule.title).toBe("Replacement Module");
});
test("should validate module before adding", () => {
const invalidModule = {
// Missing required fields
title: "Invalid Module"
};
const result = addCustomModule(invalidModule);
expect(result).toBe(false);
});
});
});