import { describe, it, expect, vi, beforeEach } from "vitest"; vi.mock("@lila/db", () => ({ getGameTerms: vi.fn(), getDistractors: vi.fn() })); import { getGameTerms, getDistractors } from "@lila/db"; import { generateMultiplayerQuestions } from "./multiplayerGameService.js"; const mockGetGameTerms = vi.mocked(getGameTerms); const mockGetDistractors = vi.mocked(getDistractors); const fakeTerms = [ { termId: "t1", sourceText: "dog", targetText: "cane", sourceGloss: null }, { termId: "t2", sourceText: "cat", targetText: "gatto", sourceGloss: null }, { termId: "t3", sourceText: "house", targetText: "casa", sourceGloss: "a building for living in", }, ]; beforeEach(() => { vi.clearAllMocks(); mockGetGameTerms.mockResolvedValue(fakeTerms); mockGetDistractors.mockResolvedValue(["wrong1", "wrong2", "wrong3"]); }); describe("generateMultiplayerQuestions", () => { it("returns the correct number of questions", async () => { const questions = await generateMultiplayerQuestions(); expect(questions).toHaveLength(3); }); it("each question has exactly 4 options", async () => { const questions = await generateMultiplayerQuestions(); for (const question of questions) { expect(question.options).toHaveLength(4); } }); it("each question has a unique questionId", async () => { const questions = await generateMultiplayerQuestions(); const ids = questions.map((q) => q.questionId); expect(new Set(ids).size).toBe(ids.length); }); it("options have sequential optionIds 0-3", async () => { const questions = await generateMultiplayerQuestions(); for (const question of questions) { const optionIds = question.options.map((o) => o.optionId); expect(optionIds).toEqual([0, 1, 2, 3]); } }); it("the correct answer is always among the options", async () => { const questions = await generateMultiplayerQuestions(); for (let i = 0; i < questions.length; i++) { const question = questions[i]!; const correctText = fakeTerms[i]!.targetText; const optionTexts = question.options.map((o) => o.text); expect(optionTexts).toContain(correctText); } }); it("correctOptionId points to the option whose text matches the correct answer", async () => { const questions = await generateMultiplayerQuestions(); for (let i = 0; i < questions.length; i++) { const question = questions[i]!; const correctText = fakeTerms[i]!.targetText; const correctOption = question.options.find( (o) => o.optionId === question.correctOptionId, ); expect(correctOption?.text).toBe(correctText); } }); it("sets the prompt from the source text", async () => { const questions = await generateMultiplayerQuestions(); expect(questions[0]!.prompt).toBe("dog"); expect(questions[1]!.prompt).toBe("cat"); expect(questions[2]!.prompt).toBe("house"); }); it("passes gloss through (null or string)", async () => { const questions = await generateMultiplayerQuestions(); expect(questions[0]!.gloss).toBeNull(); expect(questions[2]!.gloss).toBe("a building for living in"); }); it("calls getGameTerms with the multiplayer defaults", async () => { await generateMultiplayerQuestions(); expect(mockGetGameTerms).toHaveBeenCalledWith( "en", "it", "noun", "easy", 3, ); }); it("calls getDistractors once per question", async () => { await generateMultiplayerQuestions(); expect(mockGetDistractors).toHaveBeenCalledTimes(3); }); it("propagates unexpected errors from getGameTerms", async () => { mockGetGameTerms.mockRejectedValue(new Error("connection refused")); await expect(generateMultiplayerQuestions()).rejects.toThrow( "connection refused", ); }); });