/* eslint-disable max-lines-per-function -- Test suites naturally have many cases */ /* eslint-disable @typescript-eslint/consistent-type-assertions -- Tests build minimal state objects */ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { checkAndUnlockTitles, parseUnlockedTitles, } from "../../src/services/titles.js"; import type { GameState } from "@elysium/types"; const makeMinimalState = (overrides: Partial = {}): GameState => ({ player: { discordId: "t", username: "u", discriminator: "0", avatar: null, totalGoldEarned: 0, totalClicks: 0, characterName: "T" }, bosses: [], quests: [], prestige: { count: 0, runestones: 0, productionMultiplier: 1, purchasedUpgradeIds: [] }, adventurers: [], achievements: [], ...overrides, } as GameState); describe("parseUnlockedTitles", () => { it("returns the array as-is when input is a string array", () => { expect(parseUnlockedTitles(["boss_slayer", "the_adventurous"])).toEqual(["boss_slayer", "the_adventurous"]); }); it("returns empty array for null input", () => { expect(parseUnlockedTitles(null)).toEqual([]); }); it("returns empty array for undefined input", () => { expect(parseUnlockedTitles(undefined)).toEqual([]); }); it("returns empty array for object input", () => { expect(parseUnlockedTitles({ key: "value" })).toEqual([]); }); it("returns empty array for number input", () => { expect(parseUnlockedTitles(42)).toEqual([]); }); it("filters non-string values from mixed array", () => { expect(parseUnlockedTitles(["valid", 42, null, "also_valid"])).toEqual(["valid", "also_valid"]); }); it("returns empty array for an empty array", () => { expect(parseUnlockedTitles([])).toEqual([]); }); }); describe("checkAndUnlockTitles", () => { const NOW = 1_700_000_000_000; const THIRTY_DAYS_MS = 30 * 86_400_000; beforeEach(() => { vi.spyOn(Date, "now").mockReturnValue(NOW); }); afterEach(() => { vi.restoreAllMocks(); }); it("returns empty array when no new titles are earned", () => { const state = makeMinimalState(); const result = checkAndUnlockTitles({ currentUnlocked: [], state, guildName: "", createdAt: NOW }); expect(result).toEqual([]); }); it("skips titles already unlocked", () => { const state = makeMinimalState({ player: { discordId: "t", username: "u", discriminator: "0", avatar: null, totalGoldEarned: 0, totalClicks: 10_000, characterName: "T" }, }); const result = checkAndUnlockTitles({ currentUnlocked: ["click_maniac"], state, guildName: "", createdAt: NOW }); expect(result).not.toContain("click_maniac"); }); it("unlocks guild_founder when guild name is non-empty", () => { const state = makeMinimalState(); const result = checkAndUnlockTitles({ currentUnlocked: [], state, guildName: "My Guild", createdAt: NOW }); expect(result).toContain("guild_founder"); }); it("does not unlock guild_founder for whitespace-only guild name", () => { const state = makeMinimalState(); const result = checkAndUnlockTitles({ currentUnlocked: [], state, guildName: " ", createdAt: NOW }); expect(result).not.toContain("guild_founder"); }); it("unlocks the_adventurous when 1 quest is completed", () => { const state = makeMinimalState({ quests: [{ status: "completed" }] as GameState["quests"], }); const result = checkAndUnlockTitles({ currentUnlocked: [], state, guildName: "", createdAt: NOW }); expect(result).toContain("the_adventurous"); }); it("unlocks boss_slayer when 1 boss is defeated", () => { const state = makeMinimalState({ bosses: [{ status: "defeated" }] as GameState["bosses"], }); const result = checkAndUnlockTitles({ currentUnlocked: [], state, guildName: "", createdAt: NOW }); expect(result).toContain("boss_slayer"); }); it("unlocks the_undying at prestige 1", () => { const state = makeMinimalState({ prestige: { count: 1, runestones: 0, productionMultiplier: 1, purchasedUpgradeIds: [] }, }); const result = checkAndUnlockTitles({ currentUnlocked: [], state, guildName: "", createdAt: NOW }); expect(result).toContain("the_undying"); }); it("unlocks veteran after 30 days of play", () => { const createdAt = NOW - THIRTY_DAYS_MS; const state = makeMinimalState(); const result = checkAndUnlockTitles({ currentUnlocked: [], state, guildName: "", createdAt }); expect(result).toContain("veteran"); }); it("does not unlock veteran before 30 days", () => { const createdAt = NOW - (29 * 86_400_000); const state = makeMinimalState(); const result = checkAndUnlockTitles({ currentUnlocked: [], state, guildName: "", createdAt }); expect(result).not.toContain("veteran"); }); it("returns multiple newly unlocked titles at once", () => { const state = makeMinimalState({ bosses: [{ status: "defeated" }] as GameState["bosses"], quests: [{ status: "completed" }] as GameState["quests"], }); const result = checkAndUnlockTitles({ currentUnlocked: [], state, guildName: "Guild", createdAt: NOW }); expect(result).toContain("boss_slayer"); expect(result).toContain("the_adventurous"); expect(result).toContain("guild_founder"); }); it("reads transcendenceCount and apotheosisCount from state when present", () => { const state = makeMinimalState({ transcendence: { count: 1, echoes: 0, purchasedUpgradeIds: [], echoIncomeMultiplier: 1, echoCombatMultiplier: 1, echoPrestigeThresholdMultiplier: 1, echoPrestigeRunestoneMultiplier: 1, echoMetaMultiplier: 1, }, apotheosis: { count: 1 }, }); // Just verify this runs without error — the counts are read via ?. chains const result = checkAndUnlockTitles({ currentUnlocked: [], state, guildName: "", createdAt: NOW }); expect(Array.isArray(result)).toBe(true); }); });