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>
This commit is contained in:
2025-12-21 22:12:00 +01:00
parent 94cdf368bc
commit 862d29aa19
15 changed files with 1136 additions and 66 deletions

View File

@@ -1,26 +1,36 @@
import { describe, test, expect, vi, beforeEach } from "vitest";
import { loadModules, getModuleById, loadModuleFromUrl, addCustomModule } from "../../src/config/lessons.js";
// Mock the module store for testing
vi.mock("../../lessons/flexbox.json", () => ({ default: { id: "flexbox", title: "Flexbox", lessons: [] } }));
vi.mock("../../lessons/grid.json", () => ({ default: { id: "grid", title: "CSS Grid", lessons: [] } }));
vi.mock("../../lessons/00-basics.json", () => ({ default: { id: "basics", title: "CSS Basics", lessons: [] } }));
vi.mock("../../lessons/tailwindcss.json", () => ({ default: { id: "tailwind", title: "Tailwind CSS", lessons: [] } }));
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(4);
expect(modules.length).toBe(6);
// Check if modules have the right structure
const moduleIds = modules.map((m) => m.id);
expect(moduleIds).toContain("basics");
expect(moduleIds).toContain("flexbox");
expect(moduleIds).toContain("grid");
expect(moduleIds).toContain("tailwind");
// 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);
});
});
});
});
@@ -29,10 +39,10 @@ describe("Lessons Config Module", () => {
// Load modules first to populate the module store
await loadModules();
const flexboxModule = getModuleById("flexbox");
expect(flexboxModule).not.toBeNull();
expect(flexboxModule.id).toBe("flexbox");
expect(flexboxModule.title).toBe("Flexbox");
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 () => {