Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/badge-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Badge Tests

on:
pull_request:
branches: [main]
paths:
- "frontend/js/user/badges.js"
- "tests/badges.test.cjs"

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: node --test tests/badges.test.cjs
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon --ext js,html,css server.js"
"dev": "nodemon --ext js,html,css server.js",
"test": "node --test"
},
"repository": {
"type": "git",
Expand Down
105 changes: 98 additions & 7 deletions tests/badges.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const source = fs.readFileSync(
"utf8",
);

async function sprinterEarned(history) {
async function badgeEarned(id, data) {
const wall = {
innerHTML: "",
children: [],
Expand All @@ -28,25 +28,116 @@ async function sprinterEarned(history) {
`data:text/javascript;base64,${Buffer.from(source).toString("base64")}`
);

await loadBadges({ history, leaderboardRanks: {} });
const badge = wall.children.find((item) => item.textContent === "SPRINTER");
await loadBadges(data);
const badge = wall.children.find((item) => item.textContent === id);
assert.ok(badge, `${id} was not rendered`);
return !badge.className.includes("badge-locked");
}

test("SPRINTER requires seven solves between consecutive dates", async () => {
const entry = (date, easy) => ({ date, easy, medium: 0, hard: 0 });
const earned = (history) => badgeEarned("SPRINTER", { history });

assert.equal(
await sprinterEarned([entry("2026-10-01", 10), entry("2026-10-02", 17)]),
await earned([entry("2026-10-01", 10), entry("2026-10-02", 17)]),
true,
);
assert.equal(
await sprinterEarned([entry("2026-10-01", 10), entry("2026-10-02", 16)]),
await earned([entry("2026-10-01", 10), entry("2026-10-02", 16)]),
false,
);
assert.equal(
await sprinterEarned([entry("2026-10-01", 10), entry("2026-10-06", 17)]),
await earned([entry("2026-10-01", 10), entry("2026-10-06", 17)]),
false,
);
assert.equal(await sprinterEarned([entry("2026-10-01", 10)]), false);
assert.equal(await earned([entry("2026-10-01", 10)]), false);
});

test("HOT_STREAK needs a current streak of seven", async () => {
assert.equal(
await badgeEarned("HOT_STREAK", { streak: { current: 7 } }),
true,
);
assert.equal(
await badgeEarned("HOT_STREAK", { streak: { current: 6 } }),
false,
);
});

test("HARD_CARRY needs five Hard and no Easy solves in the recent window", async () => {
const history = Array.from({ length: 8 }, (_, i) => ({
date: `2026-10-${String(i + 1).padStart(2, "0")}`,
easy: 0,
medium: 0,
hard: i >= 6 ? 5 : 0,
}));

assert.equal(await badgeEarned("HARD_CARRY", { history }), true);
const withEasy = history.map((entry, i) => ({
...entry,
easy: i >= 6 ? 1 : 0,
}));
assert.equal(await badgeEarned("HARD_CARRY", { history: withEasy }), false);
const fewHard = history.map((entry, i) => ({
...entry,
hard: i >= 6 ? 4 : 0,
}));
assert.equal(await badgeEarned("HARD_CARRY", { history: fewHard }), false);
});

test("CENTURION needs at least 100 total solves", async () => {
assert.equal(
await badgeEarned("CENTURION", {
history: [{ easy: 50, medium: 30, hard: 20 }],
}),
true,
);
assert.equal(
await badgeEarned("CENTURION", {
history: [{ easy: 50, medium: 30, hard: 19 }],
}),
false,
);
});

test("BALANCED needs five of each and counts within five percent", async () => {
const earned = (easy, medium, hard) =>
badgeEarned("BALANCED", { history: [{ easy, medium, hard }] });

assert.equal(await earned(20, 20, 19), true);
assert.equal(await earned(20, 20, 18), false);
assert.equal(await earned(4, 4, 4), false);
});

test("UP_LINK needs an overall rank gain of five", async () => {
const earned = (change) =>
badgeEarned("UP_LINK", { leaderboardRanks: { overall: { change } } });

assert.equal(await earned("5"), true);
assert.equal(await earned("4"), false);
});

test("TOP_BRASS needs a valid overall rank in the top ten", async () => {
const earned = (rank) =>
badgeEarned("TOP_BRASS", { leaderboardRanks: { overall: { rank } } });

assert.equal(await earned("10"), true);
assert.equal(await earned("11"), false);
assert.equal(await earned("--"), false);
});

test("SPEEDRUN needs a top-three weekly rank and positive weighted score", async () => {
const history = [
{ easy: 0, medium: 0, hard: 0 },
{ easy: 0, medium: 1, hard: 0 },
];
const earned = (rank, entries) =>
badgeEarned("SPEEDRUN", {
history: entries,
leaderboardRanks: { weekly: { rank } },
});

assert.equal(await earned(3, history), true);
assert.equal(await earned(4, history), false);
assert.equal(await earned(3, [history[0], history[0]]), false);
});
Loading