import { describe, test, expect } from "bun:test"; import { generateShareText, getVerseSnippet } from "$lib/utils/share"; import { getBookById } from "$lib/utils/game"; import type { Guess } from "$lib/utils/game"; // Helpers to build Guess objects without calling evaluateGuess function makeGuess(bookId: string, overrides: Partial> = {}): Guess { const book = getBookById(bookId)!; return { book, testamentMatch: false, sectionMatch: false, adjacent: false, firstLetterMatch: false, ...overrides, }; } const CORRECT_BOOK_ID = "GEN"; const exactGuess = makeGuess("GEN", { testamentMatch: true, sectionMatch: true, }); const adjacentGuess = makeGuess("EXO", { testamentMatch: true, sectionMatch: true, adjacent: true, }); const sectionGuess = makeGuess("LEV", { testamentMatch: true, sectionMatch: true, }); const testamentGuess = makeGuess("JOS", { testamentMatch: true, sectionMatch: false, }); const noMatchGuess = makeGuess("MAT", { testamentMatch: false, sectionMatch: false, }); describe("generateShareText — emoji mapping", () => { test("exact match → ✅", () => { const text = generateShareText({ guesses: [exactGuess], correctBookId: CORRECT_BOOK_ID, dailyVerseDate: "2025-01-15", chapterCorrect: false, isLoggedIn: false, origin: "https://bibdle.com", verseText: "In the beginning...", }); expect(text).toContain("✅"); }); test("adjacent book → ‼️", () => { const text = generateShareText({ guesses: [adjacentGuess], correctBookId: CORRECT_BOOK_ID, dailyVerseDate: "2025-01-15", chapterCorrect: false, isLoggedIn: false, origin: "https://bibdle.com", verseText: "In the beginning...", }); expect(text).toContain("‼️"); }); test("section match → 🟩", () => { // LEV matches section (Law) but is not adjacent to GEN (order 1 vs 3) const text = generateShareText({ guesses: [sectionGuess], correctBookId: CORRECT_BOOK_ID, dailyVerseDate: "2025-01-15", chapterCorrect: false, isLoggedIn: false, origin: "https://bibdle.com", verseText: "In the beginning...", }); expect(text).toContain("🟩"); }); test("testament match only → 🟧", () => { const text = generateShareText({ guesses: [testamentGuess], correctBookId: CORRECT_BOOK_ID, dailyVerseDate: "2025-01-15", chapterCorrect: false, isLoggedIn: false, origin: "https://bibdle.com", verseText: "In the beginning...", }); expect(text).toContain("🟧"); }); test("no match → 🟥", () => { const text = generateShareText({ guesses: [noMatchGuess], correctBookId: CORRECT_BOOK_ID, dailyVerseDate: "2025-01-15", chapterCorrect: false, isLoggedIn: false, origin: "https://bibdle.com", verseText: "In the beginning...", }); expect(text).toContain("🟥"); }); }); describe("generateShareText — guess count wording", () => { test("1 guess uses singular 'guess'", () => { const text = generateShareText({ guesses: [exactGuess], correctBookId: CORRECT_BOOK_ID, dailyVerseDate: "2025-01-15", chapterCorrect: false, isLoggedIn: false, origin: "https://bibdle.com", verseText: "...", }); expect(text).toContain("1 guess,"); }); test("multiple guesses uses plural 'guesses'", () => { const text = generateShareText({ guesses: [noMatchGuess, testamentGuess, exactGuess], correctBookId: CORRECT_BOOK_ID, dailyVerseDate: "2025-01-15", chapterCorrect: false, isLoggedIn: false, origin: "https://bibdle.com", verseText: "...", }); expect(text).toContain("3 guesses,"); }); }); describe("generateShareText — streak display", () => { test("streak > 1 is shown with fire emoji", () => { const text = generateShareText({ guesses: [exactGuess], correctBookId: CORRECT_BOOK_ID, dailyVerseDate: "2025-01-15", chapterCorrect: false, isLoggedIn: false, streak: 5, origin: "https://bibdle.com", verseText: "...", }); expect(text).toContain("5 days 🔥"); }); test("streak of 1 is not shown", () => { const text = generateShareText({ guesses: [exactGuess], correctBookId: CORRECT_BOOK_ID, dailyVerseDate: "2025-01-15", chapterCorrect: false, isLoggedIn: false, streak: 1, origin: "https://bibdle.com", verseText: "...", }); expect(text).not.toContain("🔥"); }); test("undefined streak is not shown", () => { const text = generateShareText({ guesses: [exactGuess], correctBookId: CORRECT_BOOK_ID, dailyVerseDate: "2025-01-15", chapterCorrect: false, isLoggedIn: false, origin: "https://bibdle.com", verseText: "...", }); expect(text).not.toContain("🔥"); }); }); describe("generateShareText — chapter star", () => { test("1 guess + chapterCorrect → ⭐ appended to emoji line", () => { const text = generateShareText({ guesses: [exactGuess], correctBookId: CORRECT_BOOK_ID, dailyVerseDate: "2025-01-15", chapterCorrect: true, isLoggedIn: false, origin: "https://bibdle.com", verseText: "...", }); expect(text).toContain("✅ ⭐"); }); test("multiple guesses + chapterCorrect → no star (only awarded for hole-in-one)", () => { const text = generateShareText({ guesses: [noMatchGuess, exactGuess], correctBookId: CORRECT_BOOK_ID, dailyVerseDate: "2025-01-15", chapterCorrect: true, isLoggedIn: false, origin: "https://bibdle.com", verseText: "...", }); expect(text).not.toContain("⭐"); }); test("1 guess + chapterCorrect false → no star", () => { const text = generateShareText({ guesses: [exactGuess], correctBookId: CORRECT_BOOK_ID, dailyVerseDate: "2025-01-15", chapterCorrect: false, isLoggedIn: false, origin: "https://bibdle.com", verseText: "...", }); expect(text).not.toContain("⭐"); }); }); describe("generateShareText — login book emoji", () => { test("logged in uses 📜", () => { const text = generateShareText({ guesses: [exactGuess], correctBookId: CORRECT_BOOK_ID, dailyVerseDate: "2025-01-15", chapterCorrect: false, isLoggedIn: true, origin: "https://bibdle.com", verseText: "...", }); expect(text).toContain("📜"); expect(text).not.toContain("📖"); }); test("not logged in uses 📖", () => { const text = generateShareText({ guesses: [exactGuess], correctBookId: CORRECT_BOOK_ID, dailyVerseDate: "2025-01-15", chapterCorrect: false, isLoggedIn: false, origin: "https://bibdle.com", verseText: "...", }); expect(text).toContain("📖"); expect(text).not.toContain("📜"); }); }); describe("generateShareText — date formatting", () => { test("date is formatted as 'Mon DD, YYYY'", () => { const text = generateShareText({ guesses: [exactGuess], correctBookId: CORRECT_BOOK_ID, dailyVerseDate: "2025-01-15", chapterCorrect: false, isLoggedIn: false, origin: "https://bibdle.com", verseText: "...", }); expect(text).toContain("Jan 15, 2025"); }); }); describe("generateShareText — guess order", () => { test("guesses are reversed in the emoji line (first guess last)", () => { // noMatchGuess first, then exactGuess — reversed output: ✅🟥 const text = generateShareText({ guesses: [noMatchGuess, exactGuess], correctBookId: CORRECT_BOOK_ID, dailyVerseDate: "2025-01-15", chapterCorrect: false, isLoggedIn: false, origin: "https://bibdle.com", verseText: "...", }); const lines = text.split("\n"); expect(lines[2]).toBe("✅🟥"); }); }); describe("getVerseSnippet", () => { test("wraps output in curly double quotes", () => { const result = getVerseSnippet("Hello world"); expect(result.startsWith("\u201C")).toBe(true); expect(result.endsWith("\u201D")).toBe(true); }); test("short verse (fewer than 10 words) returns full text", () => { const result = getVerseSnippet("For God so loved"); // No punctuation search happens, returns all words expect(result).toContain("For God so loved"); expect(result).toContain("..."); }); test("verse with no punctuation in range returns first 25 words", () => { const words = Array.from({ length: 30 }, (_, i) => `word${i + 1}`); const verse = words.join(" "); const result = getVerseSnippet(verse); // Should contain up to 25 words expect(result).toContain("word25"); expect(result).not.toContain("word26"); }); test("truncates at punctuation between words 10 and 25", () => { // 12 words before comma, rest after const verse = "one two three four five six seven eight nine ten eleven twelve, thirteen fourteen fifteen twenty"; const result = getVerseSnippet(verse); // The comma is after word 12, which is between word 10 and 25 expect(result).toContain("twelve"); expect(result).not.toContain("thirteen"); }); test("punctuation before word 10 does not trigger truncation", () => { // Comma is after word 5 — before the search window starts at word 10 const verse = "one two three four five, six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen"; const result = getVerseSnippet(verse); // The comma at word 5 is before start of search range, so we continue // The snippet should contain word 10 at minimum expect(result).toContain("ten"); }); test("does not include trailing whitespace before ellipsis", () => { const verse = "one two three four five six seven eight nine ten eleven twelve, rest of verse here"; const result = getVerseSnippet(verse); // trimEnd is applied before adding ..., so no space before ... expect(result).not.toMatch(/\s\.\.\./); }); });