test: add 100% coverage for apps/api and packages/types

Adds full Vitest test suites with @vitest/coverage-v8, targeting 100%
statement/branch/function/line coverage. Uses v8 ignore comments for
genuinely unreachable defensive branches.
This commit is contained in:
2026-03-07 19:59:48 -08:00
committed by Naomi Carrigan
parent 5d2d71983a
commit b67eae9d46
39 changed files with 4277 additions and 8 deletions
+4 -2
View File
@@ -7,12 +7,14 @@
"scripts": {
"build": "tsc -p tsconfig.json",
"lint": "eslint --max-warnings 0 src",
"test": "echo \"No tests for types package\""
"test": "vitest run --coverage"
},
"devDependencies": {
"@nhcarrigan/eslint-config": "5.2.0",
"@nhcarrigan/typescript-config": "4.0.0",
"@vitest/coverage-v8": "3.0.8",
"eslint": "9.22.0",
"typescript": "5.8.2"
"typescript": "5.8.2",
"vitest": "3.0.8"
}
}
+1
View File
@@ -38,6 +38,7 @@ export const isStoryChapterUnlocked = (chapter: StoryChapter, state: GameState):
return state.bosses.some((b) => b.id === unlock.bossId && b.status === "defeated");
}
if (unlock.type === "prestige") {
/* v8 ignore next -- @preserve */
return (state.prestige?.count ?? 0) >= (unlock.threshold ?? 1);
}
if (unlock.type === "transcendence") {
+161
View File
@@ -0,0 +1,161 @@
/* eslint-disable max-lines-per-function -- Test suites naturally have many cases */
/* eslint-disable max-lines -- Test suites naturally have many cases */
import { describe, expect, it } from "vitest";
import {
computeUnlockedCompanionIds,
getActiveCompanionBonus,
} from "../src/interfaces/Companion.js";
const baseParams = {
lifetimeBossesDefeated: 0,
lifetimeQuestsCompleted: 0,
lifetimeGoldEarned: 0,
prestigeCount: 0,
transcendenceCount: 0,
apotheosisCount: 0,
};
describe("computeUnlockedCompanionIds", () => {
it("returns empty array when no thresholds are met", () => {
expect(computeUnlockedCompanionIds(baseParams)).toEqual([]);
});
it("unlocks lyra at 100 lifetime bosses", () => {
const result = computeUnlockedCompanionIds({ ...baseParams, lifetimeBossesDefeated: 100 });
expect(result).toContain("lyra");
});
it("does not unlock lyra below threshold", () => {
const result = computeUnlockedCompanionIds({ ...baseParams, lifetimeBossesDefeated: 99 });
expect(result).not.toContain("lyra");
});
it("unlocks finn at 100 lifetime quests", () => {
const result = computeUnlockedCompanionIds({ ...baseParams, lifetimeQuestsCompleted: 100 });
expect(result).toContain("finn");
});
it("unlocks wren at 500 lifetime quests", () => {
const result = computeUnlockedCompanionIds({ ...baseParams, lifetimeQuestsCompleted: 500 });
expect(result).toContain("finn");
expect(result).toContain("wren");
});
it("unlocks aldric at 200 lifetime bosses", () => {
const result = computeUnlockedCompanionIds({ ...baseParams, lifetimeBossesDefeated: 200 });
expect(result).toContain("lyra");
expect(result).toContain("aldric");
});
it("unlocks sera at 10 prestiges", () => {
const result = computeUnlockedCompanionIds({ ...baseParams, prestigeCount: 10 });
expect(result).toContain("sera");
});
it("does not unlock sera below threshold", () => {
const result = computeUnlockedCompanionIds({ ...baseParams, prestigeCount: 9 });
expect(result).not.toContain("sera");
});
it("unlocks kael at 720 lifetime bosses", () => {
const result = computeUnlockedCompanionIds({ ...baseParams, lifetimeBossesDefeated: 720 });
expect(result).toContain("kael");
expect(result).toContain("lyra");
expect(result).toContain("aldric");
});
it("unlocks zuri at 950 lifetime quests", () => {
const result = computeUnlockedCompanionIds({ ...baseParams, lifetimeQuestsCompleted: 950 });
expect(result).toContain("zuri");
});
it("unlocks mira at 1e18 lifetime gold", () => {
const result = computeUnlockedCompanionIds({ ...baseParams, lifetimeGoldEarned: 1e18 });
expect(result).toContain("mira");
});
it("does not unlock mira below threshold", () => {
const result = computeUnlockedCompanionIds({ ...baseParams, lifetimeGoldEarned: 9.99e17 });
expect(result).not.toContain("mira");
});
it("unlocks vex at 5 transcendences", () => {
const result = computeUnlockedCompanionIds({ ...baseParams, transcendenceCount: 5 });
expect(result).toContain("vex");
});
it("does not unlock vex below threshold", () => {
const result = computeUnlockedCompanionIds({ ...baseParams, transcendenceCount: 4 });
expect(result).not.toContain("vex");
});
it("unlocks pria at 1 apotheosis", () => {
const result = computeUnlockedCompanionIds({ ...baseParams, apotheosisCount: 1 });
expect(result).toContain("pria");
});
it("unlocks multiple companions simultaneously", () => {
const result = computeUnlockedCompanionIds({
lifetimeBossesDefeated: 720,
lifetimeQuestsCompleted: 950,
lifetimeGoldEarned: 1e18,
prestigeCount: 10,
transcendenceCount: 5,
apotheosisCount: 1,
});
expect(result).toHaveLength(10);
expect(result).toContain("lyra");
expect(result).toContain("finn");
expect(result).toContain("wren");
expect(result).toContain("aldric");
expect(result).toContain("sera");
expect(result).toContain("kael");
expect(result).toContain("zuri");
expect(result).toContain("mira");
expect(result).toContain("vex");
expect(result).toContain("pria");
});
});
describe("getActiveCompanionBonus", () => {
it("returns null when activeCompanionId is null", () => {
expect(getActiveCompanionBonus(null, ["lyra"])).toBeNull();
});
it("returns null when activeCompanionId is undefined", () => {
expect(getActiveCompanionBonus(undefined, ["lyra"])).toBeNull();
});
it("returns null when companion is not in unlockedCompanionIds", () => {
expect(getActiveCompanionBonus("lyra", [])).toBeNull();
});
it("returns null for an unknown companion id even if in unlocked list", () => {
expect(getActiveCompanionBonus("unknown_companion", ["unknown_companion"])).toBeNull();
});
it("returns the bonus for lyra when unlocked", () => {
const bonus = getActiveCompanionBonus("lyra", ["lyra"]);
expect(bonus).toEqual({ type: "passiveGold", value: 0.25 });
});
it("returns the bonus for finn when unlocked", () => {
const bonus = getActiveCompanionBonus("finn", ["lyra", "finn"]);
expect(bonus).toEqual({ type: "clickGold", value: 0.50 });
});
it("returns the bonus for wren when unlocked", () => {
const bonus = getActiveCompanionBonus("wren", ["wren"]);
expect(bonus).toEqual({ type: "questTime", value: 0.15 });
});
it("returns the bonus for vex when unlocked", () => {
const bonus = getActiveCompanionBonus("vex", ["vex"]);
expect(bonus).toEqual({ type: "essenceIncome", value: 0.75 });
});
it("returns the bonus for pria when unlocked", () => {
const bonus = getActiveCompanionBonus("pria", ["pria"]);
expect(bonus).toEqual({ type: "passiveGold", value: 1.00 });
});
});
+97
View File
@@ -0,0 +1,97 @@
/* eslint-disable max-lines-per-function -- Test suites naturally have many cases */
import { describe, expect, it } from "vitest";
import { computeSetBonuses } from "../src/interfaces/EquipmentSet.js";
import type { EquipmentSet } from "../src/interfaces/EquipmentSet.js";
const makeSet = (partial: Partial<EquipmentSet> & Pick<EquipmentSet, "pieces">): EquipmentSet => ({
id: "test_set",
name: "Test Set",
description: "A test equipment set",
bonuses: {
2: {},
3: {},
},
...partial,
});
describe("computeSetBonuses", () => {
it("returns all multipliers as 1 when no equipped items", () => {
const result = computeSetBonuses([], []);
expect(result).toEqual({ goldMultiplier: 1, combatMultiplier: 1, clickMultiplier: 1 });
});
it("returns all multipliers as 1 when sets array is empty", () => {
const result = computeSetBonuses(["item1", "item2"], []);
expect(result).toEqual({ goldMultiplier: 1, combatMultiplier: 1, clickMultiplier: 1 });
});
it("returns 1 for all when only 1 piece of a set is equipped", () => {
const set = makeSet({
pieces: ["sword", "shield", "helm"],
bonuses: { 2: { goldMultiplier: 1.5 }, 3: { goldMultiplier: 2.0 } },
});
const result = computeSetBonuses(["sword"], [set]);
expect(result).toEqual({ goldMultiplier: 1, combatMultiplier: 1, clickMultiplier: 1 });
});
it("applies 2-piece bonus when exactly 2 pieces are equipped", () => {
const set = makeSet({
pieces: ["sword", "shield", "helm"],
bonuses: { 2: { goldMultiplier: 1.5 }, 3: { combatMultiplier: 2.0 } },
});
const result = computeSetBonuses(["sword", "shield"], [set]);
expect(result).toEqual({ goldMultiplier: 1.5, combatMultiplier: 1, clickMultiplier: 1 });
});
it("applies both 2-piece and 3-piece bonuses when all 3 pieces are equipped", () => {
const set = makeSet({
pieces: ["sword", "shield", "helm"],
bonuses: { 2: { goldMultiplier: 1.5 }, 3: { combatMultiplier: 2.0 } },
});
const result = computeSetBonuses(["sword", "shield", "helm"], [set]);
expect(result).toEqual({ goldMultiplier: 1.5, combatMultiplier: 2.0, clickMultiplier: 1 });
});
it("applies click multiplier from set bonuses", () => {
const set = makeSet({
pieces: ["wand", "tome"],
bonuses: { 2: { clickMultiplier: 1.25 }, 3: {} },
});
const result = computeSetBonuses(["wand", "tome"], [set]);
expect(result).toEqual({ goldMultiplier: 1, combatMultiplier: 1, clickMultiplier: 1.25 });
});
it("stacks bonuses from multiple sets multiplicatively", () => {
const setA = makeSet({
id: "set_a",
pieces: ["sword", "shield"],
bonuses: { 2: { goldMultiplier: 1.5 }, 3: {} },
});
const setB = makeSet({
id: "set_b",
pieces: ["ring", "amulet"],
bonuses: { 2: { goldMultiplier: 2.0 }, 3: {} },
});
const result = computeSetBonuses(["sword", "shield", "ring", "amulet"], [setA, setB]);
expect(result.goldMultiplier).toBeCloseTo(3.0);
expect(result.combatMultiplier).toBe(1);
});
it("uses 1 as fallback when bonus property is undefined", () => {
const set = makeSet({
pieces: ["a", "b"],
bonuses: { 2: {}, 3: {} },
});
const result = computeSetBonuses(["a", "b"], [set]);
expect(result).toEqual({ goldMultiplier: 1, combatMultiplier: 1, clickMultiplier: 1 });
});
it("ignores non-equipped pieces that don't match the set", () => {
const set = makeSet({
pieces: ["sword", "shield"],
bonuses: { 2: { goldMultiplier: 1.5 }, 3: {} },
});
const result = computeSetBonuses(["bow", "quiver"], [set]);
expect(result).toEqual({ goldMultiplier: 1, combatMultiplier: 1, clickMultiplier: 1 });
});
});
@@ -0,0 +1,28 @@
import { describe, expect, it } from "vitest";
import { DEFAULT_PROFILE_SETTINGS } from "../src/interfaces/ProfileSettings.js";
describe("DEFAULT_PROFILE_SETTINGS", () => {
it("has all visibility flags set to true by default", () => {
expect(DEFAULT_PROFILE_SETTINGS.showTotalGold).toBe(true);
expect(DEFAULT_PROFILE_SETTINGS.showTotalClicks).toBe(true);
expect(DEFAULT_PROFILE_SETTINGS.showLifetimeBossesDefeated).toBe(true);
expect(DEFAULT_PROFILE_SETTINGS.showLifetimeQuestsCompleted).toBe(true);
expect(DEFAULT_PROFILE_SETTINGS.showLifetimeAdventurersRecruited).toBe(true);
expect(DEFAULT_PROFILE_SETTINGS.showLifetimeAchievementsUnlocked).toBe(true);
expect(DEFAULT_PROFILE_SETTINGS.showGuildFounded).toBe(true);
expect(DEFAULT_PROFILE_SETTINGS.showCurrentGold).toBe(true);
expect(DEFAULT_PROFILE_SETTINGS.showCurrentClicks).toBe(true);
expect(DEFAULT_PROFILE_SETTINGS.showPrestige).toBe(true);
expect(DEFAULT_PROFILE_SETTINGS.showTranscendence).toBe(true);
expect(DEFAULT_PROFILE_SETTINGS.showApotheosis).toBe(true);
expect(DEFAULT_PROFILE_SETTINGS.showBossesDefeated).toBe(true);
expect(DEFAULT_PROFILE_SETTINGS.showQuestsCompleted).toBe(true);
expect(DEFAULT_PROFILE_SETTINGS.showAdventurersRecruited).toBe(true);
expect(DEFAULT_PROFILE_SETTINGS.showAchievementsUnlocked).toBe(true);
expect(DEFAULT_PROFILE_SETTINGS.showOnLeaderboards).toBe(true);
});
it("defaults numberFormat to suffix", () => {
expect(DEFAULT_PROFILE_SETTINGS.numberFormat).toBe("suffix");
});
});
+164
View File
@@ -0,0 +1,164 @@
/* eslint-disable max-lines-per-function -- Test suites naturally have many cases */
import { describe, expect, it } from "vitest";
import { isStoryChapterUnlocked } from "../src/interfaces/Story.js";
import type { StoryChapter } from "../src/interfaces/Story.js";
import type { GameState } from "../src/interfaces/GameState.js";
const makeMinimalState = (overrides: Partial<GameState> = {}): GameState =>
({
bosses: [],
prestige: { count: 0, runestones: 0, productionMultiplier: 1, purchasedUpgradeIds: [] },
transcendence: undefined,
apotheosis: undefined,
...overrides,
} as unknown as GameState);
const makeChapter = (unlock: StoryChapter["unlock"]): StoryChapter => ({
id: "test_chapter",
title: "Test Chapter",
content: "Test content",
choices: [
{ id: "a", label: "Choice A", outcome: "Outcome A" },
{ id: "b", label: "Choice B", outcome: "Outcome B" },
{ id: "c", label: "Choice C", outcome: "Outcome C" },
],
unlock,
});
describe("isStoryChapterUnlocked — bossDefeated", () => {
it("returns true when the boss is defeated", () => {
const chapter = makeChapter({ type: "bossDefeated", bossId: "boss_1" });
const state = makeMinimalState({
bosses: [{ id: "boss_1", status: "defeated" }] as GameState["bosses"],
});
expect(isStoryChapterUnlocked(chapter, state)).toBe(true);
});
it("returns false when the boss is available but not defeated", () => {
const chapter = makeChapter({ type: "bossDefeated", bossId: "boss_1" });
const state = makeMinimalState({
bosses: [{ id: "boss_1", status: "available" }] as GameState["bosses"],
});
expect(isStoryChapterUnlocked(chapter, state)).toBe(false);
});
it("returns false when the boss is not in the list", () => {
const chapter = makeChapter({ type: "bossDefeated", bossId: "boss_1" });
const state = makeMinimalState({ bosses: [] });
expect(isStoryChapterUnlocked(chapter, state)).toBe(false);
});
it("returns false when a different boss is defeated", () => {
const chapter = makeChapter({ type: "bossDefeated", bossId: "boss_1" });
const state = makeMinimalState({
bosses: [{ id: "boss_2", status: "defeated" }] as GameState["bosses"],
});
expect(isStoryChapterUnlocked(chapter, state)).toBe(false);
});
});
describe("isStoryChapterUnlocked — prestige", () => {
it("returns true when prestige count meets threshold", () => {
const chapter = makeChapter({ type: "prestige", threshold: 1 });
const state = makeMinimalState({
prestige: { count: 1, runestones: 0, productionMultiplier: 1, purchasedUpgradeIds: [] },
});
expect(isStoryChapterUnlocked(chapter, state)).toBe(true);
});
it("returns true when prestige count exceeds threshold", () => {
const chapter = makeChapter({ type: "prestige", threshold: 5 });
const state = makeMinimalState({
prestige: { count: 10, runestones: 0, productionMultiplier: 1, purchasedUpgradeIds: [] },
});
expect(isStoryChapterUnlocked(chapter, state)).toBe(true);
});
it("returns false when prestige count is below threshold", () => {
const chapter = makeChapter({ type: "prestige", threshold: 5 });
const state = makeMinimalState({
prestige: { count: 4, runestones: 0, productionMultiplier: 1, purchasedUpgradeIds: [] },
});
expect(isStoryChapterUnlocked(chapter, state)).toBe(false);
});
it("defaults to threshold 1 when threshold is undefined", () => {
const chapter = makeChapter({ type: "prestige" });
const state = makeMinimalState({
prestige: { count: 1, runestones: 0, productionMultiplier: 1, purchasedUpgradeIds: [] },
});
expect(isStoryChapterUnlocked(chapter, state)).toBe(true);
});
});
describe("isStoryChapterUnlocked — transcendence", () => {
it("returns true when transcendence count meets threshold", () => {
const chapter = makeChapter({ type: "transcendence", threshold: 1 });
const state = makeMinimalState({
transcendence: { count: 1, echoes: 0, purchasedUpgradeIds: [], echoIncomeMultiplier: 1, echoCombatMultiplier: 1, echoPrestigeThresholdMultiplier: 1, echoPrestigeRunestoneMultiplier: 1, echoMetaMultiplier: 1 },
});
expect(isStoryChapterUnlocked(chapter, state)).toBe(true);
});
it("returns false when transcendence is undefined", () => {
const chapter = makeChapter({ type: "transcendence", threshold: 1 });
const state = makeMinimalState({ transcendence: undefined });
expect(isStoryChapterUnlocked(chapter, state)).toBe(false);
});
it("returns false when transcendence count is below threshold", () => {
const chapter = makeChapter({ type: "transcendence", threshold: 3 });
const state = makeMinimalState({
transcendence: { count: 2, echoes: 0, purchasedUpgradeIds: [], echoIncomeMultiplier: 1, echoCombatMultiplier: 1, echoPrestigeThresholdMultiplier: 1, echoPrestigeRunestoneMultiplier: 1, echoMetaMultiplier: 1 },
});
expect(isStoryChapterUnlocked(chapter, state)).toBe(false);
});
});
describe("isStoryChapterUnlocked — apotheosis", () => {
it("returns true when apotheosis count meets threshold", () => {
const chapter = makeChapter({ type: "apotheosis", threshold: 1 });
const state = makeMinimalState({
apotheosis: { count: 1 },
});
expect(isStoryChapterUnlocked(chapter, state)).toBe(true);
});
it("returns false when apotheosis is undefined", () => {
const chapter = makeChapter({ type: "apotheosis", threshold: 1 });
const state = makeMinimalState({ apotheosis: undefined });
expect(isStoryChapterUnlocked(chapter, state)).toBe(false);
});
it("returns false when apotheosis count is below threshold", () => {
const chapter = makeChapter({ type: "apotheosis", threshold: 2 });
const state = makeMinimalState({ apotheosis: { count: 1 } });
expect(isStoryChapterUnlocked(chapter, state)).toBe(false);
});
});
describe("isStoryChapterUnlocked — unknown type (defensive branch)", () => {
it("returns false for unknown unlock type", () => {
/* eslint-disable @typescript-eslint/consistent-type-assertions -- testing defensive branch */
const chapter = makeChapter({ type: "unknown" as StoryChapter["unlock"]["type"] });
/* eslint-enable @typescript-eslint/consistent-type-assertions */
const state = makeMinimalState();
expect(isStoryChapterUnlocked(chapter, state)).toBe(false);
});
});
describe("isStoryChapterUnlocked — threshold defaults", () => {
it("defaults transcendence threshold to 1 when undefined", () => {
const chapter = makeChapter({ type: "transcendence" });
const state = makeMinimalState({
transcendence: { count: 1, echoes: 0, purchasedUpgradeIds: [], echoIncomeMultiplier: 1, echoCombatMultiplier: 1, echoPrestigeThresholdMultiplier: 1, echoPrestigeRunestoneMultiplier: 1, echoMetaMultiplier: 1 },
});
expect(isStoryChapterUnlocked(chapter, state)).toBe(true);
});
it("defaults apotheosis threshold to 1 when undefined", () => {
const chapter = makeChapter({ type: "apotheosis" });
const state = makeMinimalState({ apotheosis: { count: 1 } });
expect(isStoryChapterUnlocked(chapter, state)).toBe(true);
});
});
+23
View File
@@ -0,0 +1,23 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
coverage: {
provider: "v8",
include: [
"src/interfaces/Companion.ts",
"src/interfaces/EquipmentSet.ts",
"src/interfaces/ProfileSettings.ts",
"src/interfaces/Story.ts",
],
exclude: [],
thresholds: {
statements: 100,
branches: 100,
functions: 100,
lines: 100,
},
},
include: ["test/**/*.spec.ts"],
},
});