From 96622779d1af43b3cbae112d5c9f07d5b3c46b85 Mon Sep 17 00:00:00 2001 From: Vahant Sharma Date: Thu, 1 Jan 2026 23:56:34 +0530 Subject: [PATCH] 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 ![correctimplementation](https://github.com/user-attachments/assets/bbbad9ab-36cf-4364-96ff-feff6ee966cd) --- resources/lang/en.json | 2 + src/client/StatsModal.ts | 10 ++- tests/client/StatsModal.test.ts | 140 ++++++++++++++++++++++++++++++++ 3 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 tests/client/StatsModal.test.ts diff --git a/resources/lang/en.json b/resources/lang/en.json index 1d6a2f2ff..15a97768e 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -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" }, diff --git a/src/client/StatsModal.ts b/src/client/StatsModal.ts index 14daef4cc..0aa3de1a7 100644 --- a/src/client/StatsModal.ts +++ b/src/client/StatsModal.ts @@ -143,10 +143,16 @@ export class StatsModal extends LitElement { ${translateText("stats_modal.games")} - + ${translateText("stats_modal.win_score")} - + ${translateText("stats_modal.loss_score")} diff --git a/tests/client/StatsModal.test.ts b/tests/client/StatsModal.test.ts new file mode 100644 index 000000000..894435510 --- /dev/null +++ b/tests/client/StatsModal.test.ts @@ -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 = { + "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).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"); + }); + }); +});