Files
erika/lib/leaderboard/ranking.test.ts
T

178 lines
4.8 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import {
buildVoiceLeaderboard,
buildXpLeaderboard,
formatInteger,
formatVoiceDuration,
normalizeRequestedPage,
shortenDiscordId,
type VoiceSourceRow,
type XpSourceRow,
} from "@/lib/leaderboard/ranking";
function xpRow(
userId: string,
totalXp: string,
level = 1,
displayName = userId,
): XpSourceRow {
return {
userId,
username: `${userId}-username`,
displayName,
level,
totalXp,
};
}
function voiceRow(
userId: string,
channelId: string,
totalSeconds: string,
displayName: string | null = userId,
): VoiceSourceRow {
return {
userId,
channelId,
totalSeconds,
displayName,
username: displayName ? `${userId}-username` : null,
};
}
describe("XP leaderboard", () => {
test("filters level zero and assigns shared competition ranks", () => {
const result = buildXpLeaderboard([
xpRow("b", "900", 2, "Beta"),
xpRow("a", "1000", 3, "Alpha"),
xpRow("c", "900", 2, "Charlie"),
xpRow("hidden", "9999", 0),
xpRow("d", "800", 1, "Delta"),
], 1);
expect(result.rows.map(({ userId, rank }) => [userId, rank])).toEqual([
["a", 1],
["b", 2],
["c", 2],
["d", 4],
]);
});
test("returns ten rows and normalizes pages beyond the result", () => {
const rows = Array.from({ length: 101 }, (_, index) =>
xpRow(String(index).padStart(3, "0"), String(1_000 - index)));
const middle = buildXpLeaderboard(rows, 2);
expect(middle.rows).toHaveLength(10);
expect(middle.rows[0]?.rank).toBe(11);
expect(middle.totalPages).toBe(11);
const last = buildXpLeaderboard(rows, 99);
expect(last.page).toBe(11);
expect(last.rows).toHaveLength(1);
});
test("uses stable name then user ID ordering for ties", () => {
const result = buildXpLeaderboard([
xpRow("3", "100", 1, "Zulu"),
xpRow("2", "100", 1, "Alpha"),
xpRow("1", "100", 1, "Alpha"),
], 1);
expect(result.rows.map((row) => row.userId)).toEqual(["1", "2", "3"]);
});
test("searches identities while preserving overall ranks", () => {
const result = buildXpLeaderboard([
xpRow("top", "1000", 4, "Alpha"),
xpRow("target", "900", 3, "Beta"),
xpRow("third", "800", 2, "Gamma"),
], 1, "target-user");
expect(result.totalRows).toBe(1);
expect(result.rows.map(({ userId, rank }) => [userId, rank])).toEqual([
["target", 2],
]);
});
});
describe("voice leaderboard", () => {
const rows = [
voiceRow("a", "voice-1", "100"),
voiceRow("a", "voice-2", "200"),
voiceRow("a", "deleted-channel", "9_999"),
voiceRow("b", "voice-1", "300"),
voiceRow("missing", "voice-2", "60", null),
];
test("aggregates current channels and excludes historical channels", () => {
const result = buildVoiceLeaderboard(rows, ["voice-1", "voice-2"], 1);
expect(result.rows.map(({ userId, totalSeconds, rank }) => ({
userId,
totalSeconds,
rank,
}))).toEqual([
{ userId: "a", totalSeconds: "300", rank: 1 },
{ userId: "b", totalSeconds: "300", rank: 1 },
{ userId: "missing", totalSeconds: "60", rank: 3 },
]);
});
test("filters to a selected current channel", () => {
const result = buildVoiceLeaderboard(
rows,
["voice-1", "voice-2"],
1,
"voice-1",
);
expect(result.rows.map(({ userId, totalSeconds }) => [
userId,
totalSeconds,
])).toEqual([
["b", "300"],
["a", "100"],
]);
});
test("keeps unresolved identities and formats their shortened ID", () => {
const result = buildVoiceLeaderboard(rows, ["voice-2"], 1);
const unresolved = result.rows.find((row) => row.userId === "missing");
expect(unresolved?.displayName).toBeNull();
expect(unresolved?.username).toBeNull();
expect(shortenDiscordId("123456789012345678")).toBe("12345…5678");
});
test("searches aggregated voice rows", () => {
const result = buildVoiceLeaderboard(
rows,
["voice-1", "voice-2"],
1,
undefined,
"missing",
);
expect(result.rows.map(({ userId, rank }) => [userId, rank])).toEqual([
["missing", 3],
]);
});
});
describe("leaderboard formatting", () => {
test("normalizes invalid page values", () => {
expect(normalizeRequestedPage(undefined)).toBe(1);
expect(normalizeRequestedPage("0")).toBe(1);
expect(normalizeRequestedPage("2.5")).toBe(1);
expect(normalizeRequestedPage(["2", "3"])).toBe(1);
expect(normalizeRequestedPage("3")).toBe(3);
});
test("formats large integer and voice duration values", () => {
expect(formatInteger("1234567")).toBe("1,234,567");
expect(formatVoiceDuration("90061")).toBe("1 วัน 1 ชม. 1 นาที");
expect(formatVoiceDuration("30")).toBe("น้อยกว่า 1 นาที");
});
});