mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-23 13:33:15 +00:00
make it a dedicated stats modal, not under account (#4627)
## Description: seperates the stats modal into a dedicated path, with gameID support e.g. : #modal=stats&gameID=VvyXpL9x ## 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
This commit is contained in:
@@ -1,4 +1,13 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
afterAll,
|
||||
afterEach,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi,
|
||||
} from "vitest";
|
||||
|
||||
vi.mock("../../src/client/Api", () => ({
|
||||
fetchPlayerById: vi.fn(async () => ({ stats: {} })),
|
||||
@@ -114,7 +123,9 @@ vi.stubGlobal("IntersectionObserver", FakeIntersectionObserver);
|
||||
|
||||
import { AccountModal } from "../../src/client/AccountModal";
|
||||
import { fetchPublicPlayerGames } from "../../src/client/Api";
|
||||
import { GameStatsModal } from "../../src/client/GameStatsModal";
|
||||
import { modalRouter } from "../../src/client/ModalRouter";
|
||||
import { initNavigation } from "../../src/client/Navigation";
|
||||
|
||||
type ModalShell = HTMLElement & {
|
||||
getScrollTop(): number;
|
||||
@@ -138,20 +149,66 @@ async function waitForModal(
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForStatsModal(
|
||||
modal: GameStatsModal,
|
||||
assertion: () => void,
|
||||
): Promise<void> {
|
||||
await vi.waitFor(async () => {
|
||||
await modal.updateComplete;
|
||||
const shell = modal.querySelector("o-modal") as ModalShell | null;
|
||||
await shell?.updateComplete;
|
||||
assertion();
|
||||
});
|
||||
}
|
||||
|
||||
describe("Account Games stats navigation", () => {
|
||||
let modal: AccountModal;
|
||||
let statsModal: GameStatsModal;
|
||||
let playPage: HTMLElement;
|
||||
|
||||
beforeAll(() => {
|
||||
playPage = document.createElement("div");
|
||||
playPage.id = "page-play";
|
||||
document.body.appendChild(playPage);
|
||||
initNavigation();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
playPage.remove();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
history.replaceState(null, "", "/");
|
||||
modalRouter.register("account", { tag: "account-modal" });
|
||||
modalRouter.register("account", {
|
||||
tag: "account-modal",
|
||||
pageId: "page-account",
|
||||
});
|
||||
modalRouter.register("stats", {
|
||||
tag: "game-stats-modal",
|
||||
pageId: "page-stats",
|
||||
});
|
||||
if (!customElements.get("account-modal")) {
|
||||
customElements.define("account-modal", AccountModal);
|
||||
}
|
||||
if (!customElements.get("game-stats-modal")) {
|
||||
customElements.define("game-stats-modal", GameStatsModal);
|
||||
}
|
||||
modal = document.createElement("account-modal") as AccountModal;
|
||||
modal.id = "page-account";
|
||||
modal.setAttribute("inline", "");
|
||||
modal.className = "hidden page-content";
|
||||
document.body.appendChild(modal);
|
||||
|
||||
statsModal = document.createElement("game-stats-modal") as GameStatsModal;
|
||||
statsModal.id = "page-stats";
|
||||
statsModal.setAttribute("inline", "");
|
||||
statsModal.className = "hidden page-content";
|
||||
document.body.appendChild(statsModal);
|
||||
|
||||
await modal.updateComplete;
|
||||
modal.open();
|
||||
await statsModal.updateComplete;
|
||||
window.showPage?.("page-account");
|
||||
await waitForModal(modal, () => {
|
||||
const shell = modal.querySelector("o-modal") as ModalShell;
|
||||
expect(shell.shadowRoot?.textContent).toContain(
|
||||
@@ -165,8 +222,9 @@ describe("Account Games stats navigation", () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
modal.close();
|
||||
window.showPage?.("page-play");
|
||||
modal.remove();
|
||||
statsModal.remove();
|
||||
history.replaceState(null, "", "/");
|
||||
});
|
||||
|
||||
@@ -179,20 +237,22 @@ describe("Account Games stats navigation", () => {
|
||||
);
|
||||
expect(statsButton).toBeTruthy();
|
||||
statsButton!.click();
|
||||
await waitForModal(modal, () => {
|
||||
expect(modal.querySelector("game-info-view")).not.toBeNull();
|
||||
await waitForStatsModal(statsModal, () => {
|
||||
expect(statsModal.isOpen()).toBe(true);
|
||||
expect(statsModal.querySelector("game-info-view")).not.toBeNull();
|
||||
});
|
||||
|
||||
expect(modal.textContent).toContain("game_list.stats");
|
||||
const statsView = modal.querySelector("game-info-view") as HTMLElement & {
|
||||
gameId: string;
|
||||
};
|
||||
expect(modal.isOpen()).toBe(false);
|
||||
expect(statsModal.textContent).toContain("game_list.stats");
|
||||
const statsView = statsModal.querySelector(
|
||||
"game-info-view",
|
||||
) as HTMLElement & { gameId: string };
|
||||
expect(statsView.gameId).toBe("game-1");
|
||||
expect(shell.shadowRoot?.querySelector('[role="tablist"]')).toBeNull();
|
||||
expect(shell.getScrollTop()).toBe(0);
|
||||
expect(window.location.hash).toBe("#modal=account&gameID=game-1");
|
||||
const statsShell = statsModal.querySelector("o-modal") as ModalShell;
|
||||
expect(statsShell.shadowRoot?.querySelector('[role="tablist"]')).toBeNull();
|
||||
expect(window.location.hash).toBe("#modal=stats&gameID=game-1");
|
||||
|
||||
const backButton = modal.querySelector(
|
||||
const backButton = statsModal.querySelector(
|
||||
'[slot="header"] button',
|
||||
) as HTMLButtonElement;
|
||||
backButton.click();
|
||||
@@ -204,28 +264,4 @@ describe("Account Games stats navigation", () => {
|
||||
|
||||
expect(fetchPublicPlayerGames).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("opens a gameID route directly and returns to Games", async () => {
|
||||
modal.setActiveTab("account");
|
||||
modal.close();
|
||||
modal.open({ gameID: "game-1" });
|
||||
|
||||
await waitForModal(modal, () => {
|
||||
const statsView = modal.querySelector("game-info-view") as
|
||||
| (HTMLElement & { gameId: string })
|
||||
| null;
|
||||
expect(statsView?.gameId).toBe("game-1");
|
||||
});
|
||||
expect(window.location.hash).toBe("#modal=account&gameID=game-1");
|
||||
|
||||
const backButton = modal.querySelector(
|
||||
'[slot="header"] button',
|
||||
) as HTMLButtonElement;
|
||||
backButton.click();
|
||||
|
||||
await waitForModal(modal, () => {
|
||||
expect(modal.querySelector("player-game-history-view")).not.toBeNull();
|
||||
});
|
||||
expect(window.location.hash).toBe("#modal=account&tab=games");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -154,6 +154,41 @@ describe("GameInfoView", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("renders a fetch error and recovers when Retry succeeds", async () => {
|
||||
const retry = deferred<AnalyticsRecord | false>();
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
try {
|
||||
fetchMock
|
||||
.mockRejectedValueOnce(new Error("network failure"))
|
||||
.mockReturnValueOnce(retry.promise);
|
||||
view = mountView("retry-game");
|
||||
|
||||
await waitForRender(view, () => {
|
||||
expect(view!.textContent).toContain("game_info_modal.load_failed");
|
||||
expect(view!.querySelector("button")?.textContent).toContain(
|
||||
"game_info_modal.retry",
|
||||
);
|
||||
});
|
||||
|
||||
const retryButton = view.querySelector("button") as HTMLButtonElement;
|
||||
retryButton.click();
|
||||
|
||||
await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(2));
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(2, "retry-game");
|
||||
|
||||
retry.resolve(makeSession("retry-game", GameMapType.Montreal));
|
||||
await retry.promise;
|
||||
await waitForRender(view, () => {
|
||||
expect(view!.textContent).toContain(GameMapType.Montreal);
|
||||
expect(view!.textContent).not.toContain("game_info_modal.load_failed");
|
||||
expect(view!.querySelectorAll("player-row")).toHaveLength(1);
|
||||
});
|
||||
} finally {
|
||||
errorSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("clears rendered game state when gameId becomes null", async () => {
|
||||
fetchMock.mockResolvedValue(makeSession("game-1", GameMapType.Montreal));
|
||||
view = mountView("game-1");
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
afterAll,
|
||||
afterEach,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi,
|
||||
} from "vitest";
|
||||
|
||||
vi.mock("../../src/client/Utils", () => ({
|
||||
translateText: vi.fn((key: string) => key),
|
||||
}));
|
||||
|
||||
vi.mock("../../src/client/components/baseComponents/stats/GameInfoView", () => {
|
||||
class FakeGameInfoView extends HTMLElement {}
|
||||
if (!customElements.get("game-info-view")) {
|
||||
customElements.define("game-info-view", FakeGameInfoView);
|
||||
}
|
||||
return { GameInfoView: FakeGameInfoView };
|
||||
});
|
||||
|
||||
import { GameStatsModal } from "../../src/client/GameStatsModal";
|
||||
import { modalRouter } from "../../src/client/ModalRouter";
|
||||
import { initNavigation } from "../../src/client/Navigation";
|
||||
|
||||
type ModalShell = HTMLElement & { updateComplete: Promise<boolean> };
|
||||
|
||||
describe("public game stats route", () => {
|
||||
let modal: GameStatsModal;
|
||||
let playPage: HTMLElement;
|
||||
|
||||
beforeAll(() => {
|
||||
playPage = document.createElement("div");
|
||||
playPage.id = "page-play";
|
||||
document.body.appendChild(playPage);
|
||||
initNavigation();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
playPage.remove();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
history.replaceState(null, "", "/");
|
||||
modalRouter.register("stats", {
|
||||
tag: "game-stats-modal",
|
||||
pageId: "page-stats",
|
||||
});
|
||||
if (!customElements.get("game-stats-modal")) {
|
||||
customElements.define("game-stats-modal", GameStatsModal);
|
||||
}
|
||||
|
||||
modal = document.createElement("game-stats-modal") as GameStatsModal;
|
||||
modal.id = "page-stats";
|
||||
modal.setAttribute("inline", "");
|
||||
modal.className = "hidden page-content";
|
||||
document.body.appendChild(modal);
|
||||
await modal.updateComplete;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.showPage?.("page-play");
|
||||
modal.remove();
|
||||
history.replaceState(null, "", "/");
|
||||
});
|
||||
|
||||
it("opens a shared gameID without mounting the authenticated account", async () => {
|
||||
history.replaceState(null, "", "/#modal=stats&gameID=public-game");
|
||||
|
||||
expect(modalRouter.routeFromHash()).toBe(true);
|
||||
|
||||
await vi.waitFor(async () => {
|
||||
await modal.updateComplete;
|
||||
const shell = modal.querySelector("o-modal") as ModalShell | null;
|
||||
await shell?.updateComplete;
|
||||
const statsView = modal.querySelector("game-info-view") as
|
||||
| (HTMLElement & { gameId: string | null })
|
||||
| null;
|
||||
expect(modal.isOpen()).toBe(true);
|
||||
expect(statsView?.gameId).toBe("public-game");
|
||||
});
|
||||
|
||||
expect(document.querySelector("account-modal")).toBeNull();
|
||||
expect(window.location.hash).toBe("#modal=stats&gameID=public-game");
|
||||
|
||||
const backButton = modal.querySelector(
|
||||
'[slot="header"] button',
|
||||
) as HTMLButtonElement;
|
||||
backButton.click();
|
||||
|
||||
expect(modal.isOpen()).toBe(false);
|
||||
expect(window.location.hash).toBe("");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user