Something went wrong. Try again.
the everything app :D cedarplan.dunkirk.sh
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331import { describe, expect, test } from "bun:test";import { buildMap } from "./map";import type { Plan } from "./planner";import { buildGraph, type CourseNode, parseRequisite } from "./prereqs";
const node = (code: string, requires: string[] = []): CourseNode => ({ code, title: code, requisites: requires.map((r) => parseRequisite({ DisplayText: `Take ${r}`, DisplayTextExtension: "- Must be completed prior to taking this course.", IsRequired: true, }), ),});
/** A plan is only ever read here, so a literal is clearer than projecting one. */const plan = (terms: [string, string[]][]): Plan => ({ terms: terms.map(([name, courses]) => ({ slot: { name, season: name.startsWith("SP") ? "spring" : "fall", year: 2000 + Number(name.slice(2)), capacity: 15, }, courses: courses.map((code) => ({ code, credits: 3 })), credits: courses.length * 3, capacity: 15, })), finishes: terms.at(-1)?.[0] ?? null, totalCredits: terms.reduce((n, [, c]) => n + c.length * 3, 0), unscheduled: [],});
const chain = buildGraph([ node("CS-1210"), node("CS-1220", ["CS-1210"]), node("CS-2210", ["CS-1220"]), node("ART-1100"),]);
describe("laying a plan out as a graph", () => { const map = buildMap( plan([ ["SP27", ["CS-1210", "ART-1100"]], ["FA27", ["CS-1220"]], ["SP28", ["CS-2210"]], ]), { graph: chain, have: new Set() }, );
test("one column per term, in order", () => { expect(map.terms.map((t) => t.name)).toEqual(["SP27", "FA27", "SP28"]); const xs = map.terms.map((t) => t.x); expect(xs).toEqual([...xs].sort((a, b) => a - b)); expect(new Set(xs).size).toBe(3); });
test("courses in a term share a column and stack down it", () => { const first = map.nodes.filter((n) => n.termName === "SP27"); expect(new Set(first.map((n) => n.x)).size).toBe(1); expect(new Set(first.map((n) => n.y)).size).toBe(2); });
test("draws an edge only between two courses that are both on the plan", () => { expect(map.edges.map((e) => `${e.from}->${e.to}`).sort()).toEqual([ "CS-1210->CS-1220", "CS-1220->CS-2210", ]); });
test("counts how many later courses wait on each one", () => { const by = new Map(map.nodes.map((n) => [n.code, n.unlocks])); expect(by.get("CS-1210")).toBe(2); expect(by.get("CS-1220")).toBe(1); expect(by.get("ART-1100")).toBe(0); });
test("marks the longest chain, which is what sets the finish date", () => { const critical = map.nodes.filter((n) => n.critical).map((n) => n.code); expect(critical.sort()).toEqual(["CS-1210", "CS-1220", "CS-2210"]); expect(map.edges.every((e) => e.critical)).toBe(true); });
test("sizes the canvas to hold every column and row", () => { const right = Math.max(...map.nodes.map((n) => n.x)); const bottom = Math.max(...map.nodes.map((n) => n.y)); expect(map.width).toBeGreaterThan(right); expect(map.height).toBeGreaterThan(bottom); });
test("edges run left to right between the boxes they join", () => { for (const edge of map.edges) { const from = map.nodes.find((n) => n.code === edge.from)!; const to = map.nodes.find((n) => n.code === edge.to)!; expect(edge.path).toStartWith(`M ${from.x + map.node.width}`); expect(edge.path).toEndWith(`${to.x} ${to.y + map.node.height / 2}`); } });});
describe("laying the same plan out downward", () => { const three = plan([ ["SP27", ["CS-1210", "ART-1100"]], ["FA27", ["CS-1220"]], ["SP28", ["CS-2210"]], ]); const down = buildMap(three, { graph: chain, have: new Set(), flow: "down" });
test("gives a term a row rather than a column", () => { const ys = down.terms.map((t) => t.y); expect(ys).toEqual([...ys].sort((a, b) => a - b)); expect(new Set(ys).size).toBe(3); // And the headings line up in one gutter down the left. expect(new Set(down.terms.map((t) => t.x)).size).toBe(1); });
test("courses in a term share a row and run across it", () => { const first = down.nodes.filter((n) => n.term === 0); expect(new Set(first.map((n) => n.y)).size).toBe(1); expect(new Set(first.map((n) => n.x)).size).toBe(2); });
test("edges leave the bottom of a box and arrive at the top of the next", () => { for (const edge of down.edges) { const from = down.nodes.find((n) => n.code === edge.from)!; const to = down.nodes.find((n) => n.code === edge.to)!; expect(edge.path).toStartWith( `M ${from.x + down.node.width / 2} ${from.y + down.node.height}`, ); expect(edge.path).toEndWith(`${to.x + down.node.width / 2} ${to.y}`); } });
test("is taller than it is wide where a degree is long and a term is not", () => { // Which is the whole reason to offer it: a browser scrolls down for free. const across = buildMap(three, { graph: chain, have: new Set() }); expect(down.height / down.width).toBeGreaterThan(across.height / across.width); });});
describe("what the graph leaves out", () => { test("a prerequisite already passed draws nothing", () => { // The graph is about the work ahead, not a history of the degree. const map = buildMap(plan([["SP27", ["CS-1220"]]]), { graph: chain, have: new Set(["CS-1210"]), }); expect(map.edges).toEqual([]); expect(map.nodes).toHaveLength(1); });
test("a plan with no chain has no critical path to point at", () => { const map = buildMap(plan([["SP27", ["ART-1100"]]]), { graph: chain, have: new Set() }); expect(map.nodes.every((n) => !n.critical)).toBe(true); });
test("an empty plan lays out without throwing", () => { const map = buildMap(plan([]), { graph: chain, have: new Set() }); expect(map.nodes).toEqual([]); expect(map.width).toBeGreaterThan(0); });
test("a cycle in the catalog terminates", () => { const loop = buildGraph([node("LOOP-1000", ["LOOP-2000"]), node("LOOP-2000", ["LOOP-1000"])]); const map = buildMap(plan([["SP27", ["LOOP-1000", "LOOP-2000"]]]), { graph: loop, have: new Set(), }); expect(map.nodes).toHaveLength(2); });});
describe("drawing the degree so far", () => { test("puts the transcript in columns before the plan", () => { const map = buildMap(plan([["SP27", ["CS-2210"]]]), { graph: chain, have: new Set(["CS-1210", "CS-1220"]), history: [ { name: "FA25", courses: [{ code: "CS-1210", credits: 3, done: true }] }, { name: "SP26", courses: [{ code: "CS-1220", credits: 3, done: true }] }, ], }); expect(map.terms.map((t) => t.name)).toEqual(["FA25", "SP26", "SP27"]); expect(map.terms.filter((t) => t.past)).toHaveLength(2); // Left to right, oldest first. expect(map.nodes.find((n) => n.code === "CS-1210")!.x).toBeLessThan( map.nodes.find((n) => n.code === "CS-2210")!.x, ); });
test("draws the edges a met prerequisite would otherwise hide", () => { // The whole reason to show history: without it CS-2210 waits on nothing // visible and the chain appears to start in the middle. const map = buildMap(plan([["SP27", ["CS-2210"]]]), { graph: chain, have: new Set(["CS-1210", "CS-1220"]), history: [ { name: "FA25", courses: [ { code: "CS-1210", credits: 3, done: true }, { code: "CS-1220", credits: 3, done: true }, ], }, ], }); expect(map.edges.map((e) => `${e.from}->${e.to}`).sort()).toEqual([ "CS-1210->CS-1220", "CS-1220->CS-2210", ]); });
test("marks what is finished apart from what is under way", () => { const map = buildMap(plan([]), { graph: chain, have: new Set(["CS-1210", "CS-1220"]), history: [ { name: "FA26", courses: [ { code: "CS-1210", credits: 3, done: true }, { code: "CS-1220", credits: 3, done: false }, ], }, ], }); expect(map.nodes.find((n) => n.code === "CS-1210")?.past).toBe("done"); expect(map.nodes.find((n) => n.code === "CS-1220")?.past).toBe("running"); });
test("counts a finished course's leverage over the work ahead", () => { const map = buildMap( plan([ ["SP27", ["CS-1220"]], ["FA27", ["CS-2210"]], ]), { graph: chain, have: new Set(["CS-1210"]), history: [{ name: "FA25", courses: [{ code: "CS-1210", credits: 3, done: true }] }], }, ); expect(map.nodes.find((n) => n.code === "CS-1210")?.unlocks).toBe(2); });});
describe("credit brought in", () => { test("is drawn apart from work done here", () => { const map = buildMap(plan([["SP27", ["CS-1220"]]]), { graph: chain, have: new Set(["CS-1210"]), history: [ { name: "transfer", transfer: true, courses: [{ code: "CS-1210", credits: 3, done: true }], }, ], }); expect(map.terms[0]).toMatchObject({ name: "transfer", past: true, transfer: true }); expect(map.nodes.find((n) => n.code === "CS-1210")?.past).toBe("transfer"); // It still gates what it gates. expect(map.edges.map((e) => `${e.from}->${e.to}`)).toEqual(["CS-1210->CS-1220"]); });});
describe("a link the requisite table never states", () => { /* * "Honors Sr Colloq I" gates its own second half as surely as any * prerequisite does — the catalog says to take them in sequence — and * Colleague records no requisite at all. Without the pairing the two sat * side by side with nothing drawn between them, and hovering either lit * nothing. */ const colloquium = buildGraph([node("HON-4910"), node("HON-4920"), node("ART-1100")]); const board = plan([ ["SP27", ["HON-4910", "ART-1100"]], ["FA27", ["HON-4920"]], ]);
test("draws nothing between them on the requisites alone", () => { const map = buildMap(board, { graph: colloquium, have: new Set() }); expect(map.edges).toHaveLength(0); });
test("and draws the sequence when it is told about it", () => { const map = buildMap(board, { graph: colloquium, have: new Set(), sequences: new Map([["HON-4920", "HON-4910"]]), }); expect(map.edges).toEqual([ expect.objectContaining({ from: "HON-4910", to: "HON-4920", sequence: true }), ]); // And it counts: the first half is genuinely holding up the second. expect(map.nodes.find((n) => n.code === "HON-4910")?.unlocks).toBe(1); });
test("and links a second sitting to the first", () => { const twice = buildGraph([node("HON-3020"), { code: "HON-3020#2", title: "", requisites: [] }]); const map = buildMap( plan([ ["SP27", ["HON-3020"]], ["FA27", ["HON-3020#2"]], ]), { graph: twice, have: new Set() }, ); expect(map.edges).toEqual([ expect.objectContaining({ from: "HON-3020", to: "HON-3020#2", sequence: true }), ]); });
test("without doubling a link the requisites already carry", () => { const map = buildMap( plan([ ["SP27", ["CS-1210"]], ["FA27", ["CS-1220"]], ]), { graph: chain, have: new Set(), sequences: new Map([["CS-1220", "CS-1210"]]), }, ); expect(map.edges).toHaveLength(1); expect(map.edges[0]?.sequence).toBeUndefined(); });});