Files
bibdle/tests/share.test.ts
George Powell e550965086 add unit tests for core game, bible, share, and stats utilities
146 tests covering evaluateGuess, grading, ordinals, bible data
integrity, section counts, share text generation, and stat
string helpers. Also fixes toOrdinal for 111-113 (was using
>= 11 && <= 13 instead of % 100 check).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 15:02:46 -05:00

340 lines
9.4 KiB
TypeScript

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<Omit<Guess, "book">> = {}): 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\.\.\./);
});
});