Files
miti99bot/tests/modules/loldle-ability/handlers.test.js
T
tiennm99 3ac06bffaa feat(loldle): add ability and splash champion-guessing modules
Closes deferred phases 04 + 05 of loldle-new-modes plan.

- loldle-ability: 5 guesses, DDragon ability icon as photo. State pins
  slot (P/Q/W/E/R) so the same icon shows every turn. Abilities pulled
  from DDragon per-champion — same source loldle.net uses at runtime.
- loldle-splash: 4 guesses, random skin splash as photo. Skin pool
  scraped from loldle.net bundle (var Ad=[…] — 172 champs × 1939 skins,
  non-chroma, matches their splash mode exactly). URLs from Riot
  DDragon CDN (no version segment, stable across patches).
- fetch-ddragon-data.js: extended to write all four JSONs in one run.
  Shares a single DDragon per-champion fetch cycle (concurrency 10).
- Credits loldle.net + Riot Games in all loldle-family READMEs.

19 new tests (503 total). Lint clean. register:dry reports 12 loldle_*
commands with no conflicts.
2026-04-24 23:58:42 +07:00

90 lines
3.0 KiB
JavaScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { createStore } from "../../../src/db/create-store.js";
import abilitiesData from "../../../src/modules/loldle-ability/abilities.json" with {
type: "json",
};
import {
handleAbility,
handleGiveup,
handleStats,
} from "../../../src/modules/loldle-ability/handlers.js";
import { loadStats } from "../../../src/modules/loldle-ability/state.js";
import { makeFakeKv } from "../../fakes/fake-kv-namespace.js";
function pinRandom(value) {
// deterministic: force Math.random() to the floor of the first entry
vi.spyOn(Math, "random").mockReturnValue(value);
}
function makeCtx({ text = "", fromId = 1, chatType = "private", chatId = 1 } = {}) {
const replies = [];
const photos = [];
return {
replies,
photos,
ctx: {
from: { id: fromId },
chat: { id: chatId, type: chatType },
message: { text },
reply: async (body, opts) => {
replies.push({ body, opts });
},
replyWithPhoto: async (url, opts) => {
photos.push({ url, opts });
},
},
};
}
describe("loldle-ability handlers — happy path", () => {
let db;
beforeEach(() => {
db = createStore("loldle-ability", { KV: makeFakeKv() });
pinRandom(0); // picks abilitiesData[0] + abilities[0]
});
it("no-arg sends a photo with DDragon icon URL and caption", async () => {
const { ctx, photos } = makeCtx();
await handleAbility(ctx, db);
expect(photos).toHaveLength(1);
expect(photos[0].url).toMatch(/^https:\/\/ddragon\.leagueoflegends\.com\//);
expect(photos[0].opts.caption).toMatch(/0\/5 guesses so far/);
});
it("correct guess wins and cites slot + ability name", async () => {
const target = abilitiesData[0];
const { ctx, replies } = makeCtx({ text: `/loldle_ability ${target.championName}` });
await handleAbility(ctx, db);
expect(replies[0].body).toContain("🎉 Got it!");
expect(replies[0].body).toContain(target.championName);
expect(replies[0].body).toMatch(/\([PQWER]\)/);
const s = await loadStats(db, 1);
expect(s).toMatchObject({ played: 1, wins: 1, streak: 1 });
});
it("wrong guess does not end the round", async () => {
const wrong = abilitiesData[1];
const { ctx, replies } = makeCtx({ text: `/loldle_ability ${wrong.championName}` });
await handleAbility(ctx, db);
expect(replies[0].body).toContain("❌");
const s = await loadStats(db, 1);
expect(s.played).toBe(0);
});
it("giveup records loss and names the ability", async () => {
await handleAbility(makeCtx().ctx, db);
const { ctx, replies } = makeCtx();
await handleGiveup(ctx, db);
expect(replies[0].body).toContain("🏳️");
expect(replies[0].body).toMatch(/\([PQWER]\)/);
const s = await loadStats(db, 1);
expect(s).toMatchObject({ played: 1, wins: 0 });
});
it("stats renders zero-state", async () => {
const { ctx, replies } = makeCtx();
await handleStats(ctx, db);
expect(replies[0].body).toContain("Played: 0");
});
});