mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-01 20:12:02 +00:00
Add tooltips to Win/Loss Score columns in clan stats (#2752)
Resolves #2508 ## Description: Adds hover tooltips to the "Win Score" and "Loss Score" column headers in the clan stats table to help players understand what these weighted scores represent. ### Changes Made - Added tooltip to **Win Score** column: "Weighted wins based on clan participation and match difficulty" - Added tooltip to **Loss Score** column: "Weighted losses based on clan participation and match difficulty" - Uses native HTML `title` attribute (follows existing codebase patterns) - Fully i18n-ready via `translateText()` - other languages will be translated via Crowdin ### Implementation Details - **Files Modified**: 2 files, 4 lines total - `resources/lang/en.json`: Added 2 tooltip translation keys - `src/client/StatsModal.ts`: Added `title` attributes to table headers - No breaking changes ### Expected Behavior When hovering over "Win Score" or "Loss Score" column headers, users see a tooltip explaining the weighted scoring system based on clan participation and match difficulty. ## 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 - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced 
This commit is contained in:
@@ -177,7 +177,9 @@
|
||||
"clan": "Clan",
|
||||
"games": "Games",
|
||||
"win_score": "Win Score",
|
||||
"win_score_tooltip": "Weighted wins based on clan participation and match difficulty",
|
||||
"loss_score": "Loss Score",
|
||||
"loss_score_tooltip": "Weighted losses based on clan participation and match difficulty",
|
||||
"win_loss_ratio": "Win/Loss",
|
||||
"rank": "Rank"
|
||||
},
|
||||
|
||||
@@ -143,10 +143,16 @@ export class StatsModal extends LitElement {
|
||||
<th class="py-2 px-2 text-right">
|
||||
${translateText("stats_modal.games")}
|
||||
</th>
|
||||
<th class="py-2 px-2 text-right">
|
||||
<th
|
||||
class="py-2 px-2 text-right"
|
||||
title=${translateText("stats_modal.win_score_tooltip")}
|
||||
>
|
||||
${translateText("stats_modal.win_score")}
|
||||
</th>
|
||||
<th class="py-2 px-2 text-right">
|
||||
<th
|
||||
class="py-2 px-2 text-right"
|
||||
title=${translateText("stats_modal.loss_score_tooltip")}
|
||||
>
|
||||
${translateText("stats_modal.loss_score")}
|
||||
</th>
|
||||
<th class="py-2 pl-2 text-right">
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { StatsModal } from "../../src/client/StatsModal";
|
||||
|
||||
// Mock the translateText function
|
||||
vi.mock("../../src/client/Utils", () => ({
|
||||
translateText: vi.fn((key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
"stats_modal.win_score_tooltip":
|
||||
"Weighted wins based on clan participation and match difficulty",
|
||||
"stats_modal.loss_score_tooltip":
|
||||
"Weighted losses based on clan participation and match difficulty",
|
||||
};
|
||||
return translations[key] || key;
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock the API module
|
||||
vi.mock("../../src/client/Api", () => ({
|
||||
getApiBase: vi.fn(() => "http://localhost:3000"),
|
||||
}));
|
||||
|
||||
// Mock fetch
|
||||
global.fetch = vi.fn();
|
||||
|
||||
describe("StatsModal", () => {
|
||||
let modal: StatsModal;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Define the custom element if not already defined
|
||||
if (!customElements.get("stats-modal")) {
|
||||
customElements.define("stats-modal", StatsModal);
|
||||
}
|
||||
modal = document.createElement("stats-modal") as StatsModal;
|
||||
document.body.appendChild(modal);
|
||||
await modal.updateComplete;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(modal);
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("Tooltip Implementation - Issue #2508", () => {
|
||||
it("should render Win Score and Loss Score columns with title attributes", async () => {
|
||||
// Mock fetch to return sample clan leaderboard data
|
||||
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
start: "2025-01-01T00:00:00Z",
|
||||
end: "2025-01-07T23:59:59Z",
|
||||
clans: [
|
||||
{
|
||||
clanTag: "[TEST]",
|
||||
games: 10,
|
||||
wins: 8,
|
||||
losses: 2,
|
||||
playerSessions: 25,
|
||||
weightedWins: 8.5,
|
||||
weightedLosses: 1.5,
|
||||
weightedWLRatio: 5.67,
|
||||
},
|
||||
{
|
||||
clanTag: "[DEMO]",
|
||||
games: 8,
|
||||
wins: 6,
|
||||
losses: 2,
|
||||
playerSessions: 20,
|
||||
weightedWins: 6.0,
|
||||
weightedLosses: 2.0,
|
||||
weightedWLRatio: 3.0,
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
// Mock the modal element's open method
|
||||
const mockModalEl = { open: vi.fn(), close: vi.fn() };
|
||||
Object.defineProperty(modal, "modalEl", {
|
||||
get: () => mockModalEl,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
// Trigger modal to load and render data
|
||||
modal.open();
|
||||
await modal.updateComplete;
|
||||
|
||||
// Wait for async loadLeaderboard to complete
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
await modal.updateComplete;
|
||||
|
||||
// Query the rendered DOM for table headers (StatsModal uses light DOM via createRenderRoot)
|
||||
const allHeaders = modal.querySelectorAll("th");
|
||||
let winScoreHeader: Element | null = null;
|
||||
let lossScoreHeader: Element | null = null;
|
||||
|
||||
// Find the headers by their text content and title attribute
|
||||
allHeaders.forEach((th) => {
|
||||
const title = th.getAttribute("title");
|
||||
if (title?.includes("Weighted wins")) {
|
||||
winScoreHeader = th;
|
||||
} else if (title?.includes("Weighted losses")) {
|
||||
lossScoreHeader = th;
|
||||
}
|
||||
});
|
||||
|
||||
// Assert that headers exist with correct tooltip text
|
||||
expect(winScoreHeader).toBeTruthy();
|
||||
expect(lossScoreHeader).toBeTruthy();
|
||||
|
||||
expect(winScoreHeader!.getAttribute("title")).toBe(
|
||||
"Weighted wins based on clan participation and match difficulty",
|
||||
);
|
||||
expect(lossScoreHeader!.getAttribute("title")).toBe(
|
||||
"Weighted losses based on clan participation and match difficulty",
|
||||
);
|
||||
});
|
||||
|
||||
it("should use translateText for tooltip internationalization", async () => {
|
||||
// Verify translation keys are correct
|
||||
const { translateText } = await import("../../src/client/Utils");
|
||||
|
||||
expect(translateText("stats_modal.win_score_tooltip")).toBe(
|
||||
"Weighted wins based on clan participation and match difficulty",
|
||||
);
|
||||
expect(translateText("stats_modal.loss_score_tooltip")).toBe(
|
||||
"Weighted losses based on clan participation and match difficulty",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Modal Functionality", () => {
|
||||
it("should initialize with default state", () => {
|
||||
expect(modal).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should be a custom element", () => {
|
||||
expect(modal).toBeInstanceOf(StatsModal);
|
||||
expect(modal.tagName.toLowerCase()).toBe("stats-modal");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user