import { expect, test } from "bun:test"; import { renderToString } from "react-dom/server"; import { MemoryRouter } from "react-router-dom"; import { learnerWeb } from "../src/web"; import { filterCourses, LibraryView } from "../src/web/library/LibraryPage"; import { CourseFrame, moduleNumber } from "../src/web/shell/CourseFrame"; import { nextLesson } from "../src/web/course/pages"; import { safeNext } from "../src/web/account/AccountPage"; import type { CourseCard, CoursePayload, LibraryPayload } from "../src/shared/types"; const card = (id: string, over: Partial = {}): CourseCard => ({ id, title: id.toUpperCase(), pitch: `${id} pitch`, authors: [], coverUrl: null, accent: null, formats: ["text"], lessonCount: 3, minutes: 30, collections: [], tracks: [], source: "authored", addedAt: "2026-10-01T00:00:00Z", status: "ok", progress: 0, doneCount: 0, started: false, ...over, }); const lib: LibraryPayload = { signedIn: true, featured: card("snake", { formats: ["text", "exercises"], minutes: 900 }), continue: [{ courseId: "snake", lessonId: "m3", lessonTitle: "Pure game rules", moduleTitle: "Pure game rules", moduleIndex: 3, moduleDone: 1, moduleTotal: 5, progress: 0.2 }], newest: ["docker", "snake"], collections: [{ id: "devops", title: "DevOps", courseIds: ["docker"] }], courses: [card("docker", { formats: ["video", "slides"], minutes: 300, started: true, progress: 0.5 }), card("snake", { formats: ["text", "exercises"], minutes: 900 })], }; test("the learner web feature registers its routes", () => { expect(learnerWeb.routes.map((r) => r.path)).toEqual(["/", "/c/:courseId", "/c/:courseId/notes", "/c/:courseId/:lessonId", "/account", "/account/admin"]); }); test("every route module loads and has a default component", async () => { for (const r of learnerWeb.routes) expect(typeof (await r.element()).default).toBe("function"); }); test("library: featured course, continue row, browsable rows and the course list", () => { const out = renderToString(); expect(out).toContain("Featured"); expect(out).toContain("SNAKE"); expect(out).toContain("Next up: Pure game rules"); expect(out).toContain("New in the library"); expect(out).toContain("DevOps"); expect(out).toContain("All courses"); expect(out).toContain('href="/c/snake/m3"'); expect(out).not.toMatch(/price|rating|bestseller/i); }); test("library: search and filters switch to a results list", () => { const out = renderToString(); expect(out).toContain("Courses matching “dock”"); expect(out).not.toContain("New in the library"); }); test("library filters by format, length and in-progress", () => { const cs = lib.courses; expect(filterCourses(cs, { format: "video", length: "any", inProgress: false }).map((c) => c.id)).toEqual(["docker"]); expect(filterCourses(cs, { format: "all", length: "long", inProgress: false }).map((c) => c.id)).toEqual(["snake"]); expect(filterCourses(cs, { format: "all", length: "medium", inProgress: false }).map((c) => c.id)).toEqual(["docker"]); expect(filterCourses(cs, { format: "all", length: "any", inProgress: true }).map((c) => c.id)).toEqual(["docker"]); }); const course: CoursePayload = { id: "sd", title: "Systemic Design", pitch: null, authors: [], accent: "#86c7a6", coverUrl: null, tracks: [{ id: "rust", label: "Rust" }, { id: "ts", label: "TypeScript" }], gating: "sequential", source: "authored", status: "ok", modules: [ { id: "m00", title: "Intro", index: 0, locked: false, hasCode: false, lessons: [{ id: "a", title: "A", type: "text", minutes: 5, hasDeck: false, browser: false }] }, { id: "m01", title: "Next", index: 1, locked: true, hasCode: false, lessons: [{ id: "b", title: "Exercise 1.1 — `move`", type: "exercise", minutes: 15, hasDeck: false, browser: false }] }, ], progress: { a: { state: "done", positionSeconds: null, updatedAt: "" } }, canEdit: false, signedIn: true, diagnostics: [], }; test("course frame: outline with progress, locks, current lesson and course accent", () => { const out = renderToString(

body

, ); expect(out).toContain("--accent:#86c7a6"); expect(out).toContain('aria-current="page"'); expect(out).toContain("lr-les done"); expect(out).toContain("(locked)"); expect(out).toContain('aria-label="Course progress"'); expect(out).toContain("Your notes"); }); test("module numbers count from 0 when the first module id ends in 0", () => { expect(moduleNumber(course, 1)).toBe(1); expect(moduleNumber({ modules: [{ id: "m1" }] }, 0)).toBe(1); }); test("next lesson skips done lessons and stops at a locked module", () => { expect(nextLesson(course)).toEqual(course.modules[0]!.lessons[0]!); const open = { ...course, modules: course.modules.map((m) => ({ ...m, locked: false })) }; expect(nextLesson(open)!.id).toBe("b"); }); test("sign-in only returns to same-site paths", () => { expect(safeNext("/c/x")).toBe("/c/x"); expect(safeNext("//evil.example")).toBe("/"); expect(safeNext("https://evil.example")).toBe("/"); expect(safeNext(null)).toBe("/"); });