feat: add JavaScript lesson section with starter lessons and sidebar section grouping headers

Implementation following plan:
- S01: Update JSON schema to support 'javascript' mode
- S02: Install @codemirror/lang-javascript dependency
- S03: Define JavaScript section in sections.js
- S04: Create 3 JavaScript lesson JSON files (variables, DOM, events)
- S05: Add JavaScript validation support in validator.js
- S06: Add JavaScript preview rendering in LessonEngine.js
- S07: Add JavaScript CodeMirror mode and editor config
- S08: Register JavaScript modules in all language stores
- S09: Add JavaScript section to landing page, navigation, and app config
- S10: Add sidebar section grouping headers with category mapping
- S11: Update tests for JavaScript mode and section headers
This commit is contained in:
2026-03-28 14:03:45 +01:00
parent 7ab095718b
commit 26b9b99937
18 changed files with 859 additions and 10 deletions

View File

@@ -27,7 +27,35 @@ describe("Lessons Config Module", () => {
modules.forEach((module) => {
module.lessons.forEach((lesson) => {
expect(lesson.mode).toBeDefined();
expect(["html", "css", "tailwind", "markdown", "playground"]).toContain(lesson.mode);
expect(["html", "css", "tailwind", "markdown", "javascript", "playground"]).toContain(lesson.mode);
});
});
});
});
describe("JavaScript modules", () => {
test("should include JavaScript modules", async () => {
const modules = await loadModules();
const moduleIds = modules.map((m) => m.id);
expect(moduleIds).toContain("js-variables");
expect(moduleIds).toContain("js-dom");
expect(moduleIds).toContain("js-events");
});
test("JavaScript modules should have correct mode and structure", async () => {
const modules = await loadModules();
const jsModules = modules.filter((m) => m.mode === "javascript");
expect(jsModules.length).toBe(3);
jsModules.forEach((module) => {
expect(module.lessons.length).toBeGreaterThanOrEqual(3);
module.lessons.forEach((lesson) => {
expect(lesson.mode).toBe("javascript");
expect(lesson.validations.length).toBeGreaterThan(0);
expect(lesson.task).toBeTruthy();
expect(lesson.solution).toBeTruthy();
});
});
});

View File

@@ -240,4 +240,67 @@ describe("Renderer Module", () => {
expect(computeLessonDifficulty({ codePrefix: null })).toBe("medium");
});
});
describe("renderModuleList section headers", () => {
const noop = () => {};
test("inserts section header elements between different category groups", () => {
const container = document.getElementById("module-list");
const modules = [
{ id: "css-basic-selectors", title: "CSS Selectors", lessons: [{ title: "L1" }] },
{ id: "colors", title: "Colors", lessons: [{ title: "L1" }] },
{ id: "flexbox", title: "Flexbox", lessons: [{ title: "L1" }] },
{ id: "html-elements", title: "HTML Elements", lessons: [{ title: "L1" }] }
];
renderModuleList(container, modules, noop, noop);
const headers = container.querySelectorAll(".module-section-header");
expect(headers.length).toBe(3); // CSS Basics, CSS Layout, HTML Structure
});
test("section headers display correct category text", () => {
const container = document.getElementById("module-list");
const modules = [
{ id: "css-basic-selectors", title: "CSS Selectors", lessons: [{ title: "L1" }] },
{ id: "flexbox", title: "Flexbox", lessons: [{ title: "L1" }] }
];
renderModuleList(container, modules, noop, noop);
const headers = container.querySelectorAll(".module-section-header");
expect(headers[0].textContent).toBe("CSS Basics");
expect(headers[1].textContent).toBe("CSS Layout");
});
test("no section header is inserted between modules in the same category", () => {
const container = document.getElementById("module-list");
const modules = [
{ id: "css-basic-selectors", title: "CSS Selectors", lessons: [{ title: "L1" }] },
{ id: "colors", title: "Colors", lessons: [{ title: "L1" }] },
{ id: "typography", title: "Typography", lessons: [{ title: "L1" }] }
];
renderModuleList(container, modules, noop, noop);
const headers = container.querySelectorAll(".module-section-header");
expect(headers.length).toBe(1);
expect(headers[0].textContent).toBe("CSS Basics");
});
test("Welcome and Outro modules have no section headers", () => {
const container = document.getElementById("module-list");
const modules = [
{ id: "welcome", title: "Welcome", lessons: [{ title: "L1" }] },
{ id: "css-basic-selectors", title: "CSS Selectors", lessons: [{ title: "L1" }] },
{ id: "playground", title: "Playground", lessons: [{ title: "L1" }] }
];
renderModuleList(container, modules, noop, noop);
const headers = container.querySelectorAll(".module-section-header");
expect(headers.length).toBe(1);
expect(headers[0].textContent).toBe("CSS Basics");
});
});
});

View File

@@ -226,6 +226,69 @@ describe("CSS Validator", () => {
});
});
describe("JavaScript Validator", () => {
describe("validateUserCode with mode: javascript", () => {
it("should pass contains validation for correct code", () => {
const userCode = 'const name = "Ada";';
const lesson = {
mode: "javascript",
validations: [{ type: "contains", value: "const", message: "Use const" }]
};
const result = validateUserCode(userCode, lesson);
expect(result.isValid).toBe(true);
});
it("should fail contains validation for missing code", () => {
const userCode = 'var name = "Ada";';
const lesson = {
mode: "javascript",
validations: [{ type: "contains", value: "const", message: "Use const keyword" }]
};
const result = validateUserCode(userCode, lesson);
expect(result.isValid).toBe(false);
expect(result.message).toBe("Use const keyword");
});
it("should pass regex validation", () => {
const userCode = 'const name = "Ada";';
const lesson = {
mode: "javascript",
validations: [{ type: "regex", value: "const\\s+name\\s*=", message: "Declare name" }]
};
const result = validateUserCode(userCode, lesson);
expect(result.isValid).toBe(true);
});
it("should handle not_contains validation", () => {
const userCode = "let score = 0;";
const lesson = {
mode: "javascript",
validations: [{ type: "not_contains", value: "var", message: "Don't use var" }]
};
const result = validateUserCode(userCode, lesson);
expect(result.isValid).toBe(true);
const failLesson = {
mode: "javascript",
validations: [{ type: "not_contains", value: "let", message: "Don't use let" }]
};
const failResult = validateUserCode(userCode, failLesson);
expect(failResult.isValid).toBe(false);
});
it("should pass with no validations", () => {
const result = validateUserCode("const x = 1;", { mode: "javascript" });
expect(result.isValid).toBe(true);
expect(result.message).toContain("No validations specified");
});
});
});
describe("HTML Validator", () => {
describe("validateUserCode with mode: html", () => {
it("should validate element_exists correctly", () => {