Files
OpenFrontIO/tests/TeamStats.test.ts
T
RyanandGitHub d76a0c4009 feat(client): configurable leaderboard and team stats columns (#4646)
## Description:

- rips out some duped code, making tables use the same layout, where the
only thing different is the row content.
- updated text to be emojis to save space
- updated columns to be as wide as they need to be
- updated cells to have left/right thin borders to read easier
- added more supported column types
- removed the + button to always show everyone, where the current player
is either at the bottom or in the top 5
- added a cogwheel to change options
- can select different options for each menu type (player/team stats)

<img width="582" height="630" alt="image"
src="https://github.com/user-attachments/assets/e54dc3d5-eb2c-4bab-8732-06bea68aec45"
/>

works fine on mobile, will probably need some future love though (has
h-scroll, so all options can be seen by scrolling):


## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory

## Please put your Discord username so you can be contacted if a bug or
regression is found:

w.o.n
2026-07-22 13:13:43 -07:00

121 lines
3.7 KiB
TypeScript

import type { ColumnDef } from "../src/client/hud/layers/lib/StatsColumns";
import {
aggregateTeamValues,
TeamStats,
} from "../src/client/hud/layers/TeamStats";
import type { GameView, PlayerView } from "../src/client/view";
import { PlayerType } from "../src/core/game/Game";
import { UserSettings } from "../src/core/game/UserSettings";
import { playerInfo, setup } from "./util/Setup";
describe("aggregateTeamValues", () => {
it("sums values for alive players only", async () => {
const game = await setup("plains", {}, [
playerInfo("alive", PlayerType.Human),
playerInfo("other-alive", PlayerType.Human),
playerInfo("dead", PlayerType.Human),
]);
const alivePlayer = game.player("alive");
const otherAlivePlayer = game.player("other-alive");
const deadPlayer = game.player("dead");
for (let x = 0; x < 12; x++) alivePlayer.conquer(game.ref(x, 0));
for (let x = 0; x < 8; x++) otherAlivePlayer.conquer(game.ref(x, 1));
alivePlayer.addGold(50n);
otherAlivePlayer.addGold(30n);
deadPlayer.addGold(100n);
const selected: ColumnDef[] = [
{
id: "tiles",
labelKey: "leaderboard.owned",
valueAlignment: "end",
value: (player) => player.numTilesOwned(),
renderValue: (value) => `tiles:${value}`,
},
{
id: "gold",
labelKey: "leaderboard.gold",
valueAlignment: "end",
value: (player) => Number(player.gold()),
renderValue: (value) => `gold:${value}`,
},
];
expect(
Object.fromEntries(
aggregateTeamValues(
game.allPlayers() as unknown as PlayerView[],
selected,
game as unknown as GameView,
),
),
).toEqual({ tiles: 20, gold: 80 });
});
});
describe("TeamStats", () => {
beforeEach(() => {
localStorage.clear();
(
UserSettings as unknown as { cache: Map<string, string | null> }
).cache.clear();
});
it("renders team ranks with the shared stats table", async () => {
const players = [
{
id: () => "blue-player",
team: () => "Blue",
numTilesOwned: () => 10,
gold: () => 10n,
isAlive: () => true,
},
{
id: () => "red-player",
team: () => "Red",
numTilesOwned: () => 5,
gold: () => 20n,
isAlive: () => true,
},
] as unknown as PlayerView[];
const game = {
myPlayer: () => players[0],
playerViews: () => players,
config: () => ({ maxTroops: () => 100 }),
numLandTiles: () => 100,
numTilesWithFallout: () => 0,
} as unknown as GameView;
const teamStats = new TeamStats();
teamStats.game = game;
teamStats.visible = true;
document.body.append(teamStats);
teamStats.refresh();
await teamStats.updateComplete;
const table = teamStats.querySelector(".stats-table");
const picker = teamStats.querySelector("column-picker");
const rowCells = teamStats.querySelectorAll(
'.stats-table-row > [role="cell"]',
);
expect(teamStats.children).toHaveLength(1);
expect(picker?.closest(".stats-table")).toBe(table);
expect(table?.querySelector(".stats-table-header")).not.toBeNull();
expect(rowCells[0]?.textContent?.trim()).toBe("1");
expect(rowCells[1]?.textContent?.trim()).toContain("Blue");
const headers = teamStats.querySelectorAll(
'.stats-table-header > [role="columnheader"]',
);
(headers[3].querySelector("button") as HTMLButtonElement).click();
await teamStats.updateComplete;
expect(
teamStats.querySelectorAll('.stats-table-row > [role="cell"]')[1]
?.textContent,
).toContain("Red");
teamStats.remove();
});
});