mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-23 22:05:24 +00:00
## 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
129 lines
3.9 KiB
TypeScript
129 lines
3.9 KiB
TypeScript
import { PlayerStats } from "../src/client/hud/layers/PlayerStats";
|
|
import type { GameView, PlayerView } from "../src/client/view";
|
|
import { UserSettings } from "../src/core/game/UserSettings";
|
|
|
|
function player(id: string, tiles: number): PlayerView {
|
|
return {
|
|
id: () => id,
|
|
displayName: () => id,
|
|
numTilesOwned: () => tiles,
|
|
gold: () => BigInt(tiles),
|
|
troops: () => tiles,
|
|
totalUnitLevels: () => tiles,
|
|
isAlive: () => true,
|
|
isOnSameTeam: () => false,
|
|
} as unknown as PlayerView;
|
|
}
|
|
|
|
function gameWith(players: PlayerView[], me: PlayerView | null): GameView {
|
|
return {
|
|
myPlayer: () => me,
|
|
playerViews: () => players,
|
|
config: () => ({ maxTroops: () => 100 }),
|
|
numLandTiles: () => 100,
|
|
numTilesWithFallout: () => 0,
|
|
} as unknown as GameView;
|
|
}
|
|
|
|
async function mount(game: GameView): Promise<PlayerStats> {
|
|
const playerStats = new PlayerStats();
|
|
playerStats.game = game;
|
|
playerStats.visible = true;
|
|
document.body.append(playerStats);
|
|
playerStats.refresh();
|
|
await playerStats.updateComplete;
|
|
return playerStats;
|
|
}
|
|
|
|
function scrollRows(el: PlayerStats): string[] {
|
|
return [...el.querySelectorAll(".stats-table-scroll .stats-table-row")].map(
|
|
(row) => row.textContent?.trim().split(/\s+/)[0] ?? "",
|
|
);
|
|
}
|
|
|
|
// jsdom reports zero element sizes, so the component falls back to its
|
|
// defaults: 24px rows, 180px viewport, 4 overscan rows. The expected
|
|
// window is ceil(180 / 24) + 4 = 12 rows at scrollTop 0.
|
|
describe("StatsTable virtualization", () => {
|
|
beforeEach(() => {
|
|
localStorage.clear();
|
|
(
|
|
UserSettings as unknown as { cache: Map<string, string | null> }
|
|
).cache.clear();
|
|
});
|
|
|
|
afterEach(() => {
|
|
document.body.innerHTML = "";
|
|
});
|
|
|
|
it("renders only a window of rows for large player lists", async () => {
|
|
const players = Array.from({ length: 300 }, (_, i) =>
|
|
player(`p${i}`, 1000 - i),
|
|
);
|
|
const el = await mount(gameWith(players, null));
|
|
|
|
expect(scrollRows(el)).toEqual(
|
|
Array.from({ length: 12 }, (_, i) => `${i + 1}`),
|
|
);
|
|
const bottomSpacer = el.querySelector(
|
|
".stats-table-scroll .stats-table-spacer:last-child",
|
|
);
|
|
expect(bottomSpacer?.getAttribute("style")).toContain(
|
|
`height: ${(300 - 12) * 24}px`,
|
|
);
|
|
|
|
el.remove();
|
|
});
|
|
|
|
it("reveals later rows with a top spacer when scrolled", async () => {
|
|
const players = Array.from({ length: 300 }, (_, i) =>
|
|
player(`p${i}`, 1000 - i),
|
|
);
|
|
const el = await mount(gameWith(players, null));
|
|
|
|
const scroller = el.querySelector<HTMLElement>(".stats-table-scroll")!;
|
|
scroller.scrollTop = 240;
|
|
scroller.dispatchEvent(new Event("scroll"));
|
|
await el.updateComplete;
|
|
|
|
// first = floor(240/24) - 4 = 6; last = ceil((240+180)/24) + 4 = 22
|
|
expect(scrollRows(el)).toEqual(
|
|
Array.from({ length: 16 }, (_, i) => `${i + 7}`),
|
|
);
|
|
const topSpacer = scroller.querySelector(".stats-table-spacer");
|
|
expect(topSpacer?.getAttribute("style")).toContain(`height: ${6 * 24}px`);
|
|
|
|
el.remove();
|
|
});
|
|
|
|
it("renders the pinned row even when it is far outside the window", async () => {
|
|
const players = Array.from({ length: 299 }, (_, i) =>
|
|
player(`p${i}`, 1000 - i),
|
|
);
|
|
const me = player("me", 1);
|
|
players.push(me);
|
|
const el = await mount(gameWith(players, me));
|
|
|
|
const pinned = el.querySelector(".stats-table-pinned-row");
|
|
expect(pinned?.textContent).toContain("me");
|
|
expect(pinned?.textContent?.trim().split(/\s+/)[0]).toBe("300");
|
|
expect(scrollRows(el)).toEqual(
|
|
Array.from({ length: 12 }, (_, i) => `${i + 1}`),
|
|
);
|
|
|
|
el.remove();
|
|
});
|
|
|
|
it("renders small lists fully without spacers", async () => {
|
|
const players = Array.from({ length: 7 }, (_, i) =>
|
|
player(`p${i}`, 100 - i),
|
|
);
|
|
const el = await mount(gameWith(players, null));
|
|
|
|
expect(scrollRows(el)).toEqual(["1", "2", "3", "4", "5", "6", "7"]);
|
|
expect(el.querySelector(".stats-table-spacer")).toBeNull();
|
|
|
|
el.remove();
|
|
});
|
|
});
|