Initial addition of the Arcade study feature. Add connections as the first working game.
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m4s

This commit is contained in:
Elijah 2026-07-13 17:35:18 -07:00
parent 7b90409f2e
commit 611a585757
43 changed files with 5990 additions and 68 deletions

View file

@ -0,0 +1,74 @@
import { describe, expect, it } from "vitest";
import { ArcadeImportError, parseConnectionsImport, parseConnectionsImportBatch } from "@/lib/arcade/connectionsImport";
function validPack() {
return {
schemaVersion: 1,
type: "connections",
name: "Medication Connections",
description: "Four medication groups",
settings: { groupCount: 4, itemsPerGroup: 4, allowedMistakes: 4 },
content: Array.from({ length: 4 }, (_, groupIndex) => ({
category: `Category ${groupIndex + 1}`,
items: Array.from({ length: 4 }, (_, itemIndex) => `Item ${groupIndex + 1}-${itemIndex + 1}`),
explanation: `Explanation ${groupIndex + 1}`,
})),
};
}
describe("Connections imports", () => {
it("normalizes a valid board and creates stable ids", () => {
const result = parseConnectionsImport(JSON.stringify(validPack()));
expect(result.preview.itemCount).toBe(16);
expect(result.preview.itemCount).toBe(16);
expect(result.preview.normalized.content[0].id).toBe("group-1");
expect(result.preview.normalized.content[0].items[0].id).toBe("group-1-item-1");
});
it("rejects fences and surrounding prose", () => {
expect(() => parseConnectionsImport(`\`\`\`json\n${JSON.stringify(validPack())}\n\`\`\``)).toThrow(ArcadeImportError);
expect(() => parseConnectionsImport(`Here is the pack: ${JSON.stringify(validPack())}`)).toThrow(ArcadeImportError);
});
it("rejects duplicate tiles case-insensitively", () => {
const pack = validPack();
pack.content[1].items[0] = " item 1-1 ";
expect(() => parseConnectionsImport(JSON.stringify(pack))).toThrow("Every tile must be unique");
});
it("rejects unknown keys", () => {
const pack = { ...validPack(), extra: true };
expect(() => parseConnectionsImport(JSON.stringify(pack))).toThrow("does not match schema version 1");
});
it("reports long-tile warnings without changing the content", () => {
const pack = validPack();
pack.content[0].items[0] = "This intentionally long tile contains too many words";
const result = parseConnectionsImport(JSON.stringify(pack));
expect(result.report.warnings.length).toBeGreaterThan(0);
expect(result.preview.normalized.content[0].items[0].text).toBe(pack.content[0].items[0]);
});
it("does not treat a shared generic word as a similar category", () => {
const pack = validPack();
pack.content[0].category = "ACE Inhibitors";
pack.content[1].category = "Proton Pump Inhibitors";
const result = parseConnectionsImport(JSON.stringify(pack));
expect(result.report.warnings).not.toContain(
"Categories “ACE Inhibitors” and “Proton Pump Inhibitors” may be too similar."
);
});
it("previews multiple packs from one JSON array", () => {
const first = validPack();
const second = { ...validPack(), name: "Second board" };
const result = parseConnectionsImportBatch(JSON.stringify([first, second]));
expect(result.count).toBe(2);
expect(result.packs.map((pack) => pack.name)).toEqual(["Medication Connections", "Second board"]);
});
it("limits batch imports to ten packs", () => {
const batch = Array.from({ length: 11 }, () => validPack());
expect(() => parseConnectionsImportBatch(JSON.stringify(batch))).toThrow("between 1 and 10");
});
});