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"); + }); + }); +});