Files
OpenFrontIO/tests/client/SkinTestGameFlow.test.ts
T
2026-05-27 00:17:25 +01:00

231 lines
6.0 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../../src/client/Utils", () => ({
translateText: (k: string) => k,
getSvgAspectRatio: async () => 1,
}));
vi.mock("../../src/client/sound/SoundManager", () => ({
SoundManager: vi.fn().mockImplementation(() => ({
playBackgroundMusic: vi.fn(),
stopBackgroundMusic: vi.fn(),
})),
}));
const fetchCosmeticsMock = vi.fn();
const purchaseCosmeticMock = vi.fn();
vi.mock("../../src/client/Cosmetics", () => ({
fetchCosmetics: (...args: any[]) => fetchCosmeticsMock(...args),
purchaseCosmetic: (...args: any[]) => purchaseCosmeticMock(...args),
patternRelationship: () => "blocked",
resolveCosmetics: () => [],
}));
// Mock CosmeticButton so the modal can render a clickable purchase target in
// JSDOM without dragging in real canvas rendering.
vi.mock("../../src/client/components/CosmeticButton", () => {
class CosmeticButton extends HTMLElement {
private _resolved: any = null;
private _onPurchase?: (resolved: any, method: string) => void;
get resolved() {
return this._resolved;
}
set resolved(v: any) {
this._resolved = v;
this.renderBtn();
}
get onPurchase() {
return this._onPurchase;
}
set onPurchase(v: ((resolved: any, method: string) => void) | undefined) {
this._onPurchase = v;
this.renderBtn();
}
connectedCallback() {
this.renderBtn();
}
renderBtn() {
this.innerHTML = "";
if (this._resolved && this._onPurchase) {
const btn = document.createElement("button");
btn.setAttribute("data-testid", "buy-skin");
btn.textContent = "territory_patterns.purchase";
btn.addEventListener("click", (e) => {
e.stopPropagation();
this._onPurchase?.(this._resolved, "dollar");
});
this.appendChild(btn);
}
}
}
if (!customElements.get("cosmetic-button")) {
customElements.define("cosmetic-button", CosmeticButton);
}
return { CosmeticButton };
});
import { ClientGameRunner } from "../../src/client/ClientGameRunner";
import { SkinTestWinModal } from "../../src/client/hud/layers/SkinTestWinModal";
import { GameUpdateType } from "../../src/core/game/GameUpdates";
const makeCosmetics = () =>
({
patterns: {
purch_pattern: {
name: "purch_pattern",
affiliateCode: "aff",
pattern: "AQID",
product: { price: "$1.00", priceId: "price_test" },
colorPalettes: [],
},
},
colorPalettes: {},
}) as any;
describe("Skin test game flow", () => {
let modal: SkinTestWinModal;
let runner: ClientGameRunner | null = null;
beforeEach(async () => {
fetchCosmeticsMock.mockResolvedValue(makeCosmetics());
if (!customElements.get("skin-test-win-modal")) {
customElements.define("skin-test-win-modal", SkinTestWinModal);
}
modal = document.createElement("skin-test-win-modal") as SkinTestWinModal;
document.body.appendChild(modal);
await modal.updateComplete;
});
afterEach(() => {
runner?.stop();
runner = null;
document.body.removeChild(modal);
vi.clearAllMocks();
});
it("shows the buy modal on game-end and routes purchase through the modal", async () => {
const { EventBus } = await import("../../src/core/EventBus");
const eventBus = new EventBus();
const renderer = {
initialize: vi.fn(),
tick: vi.fn(),
skinTestWinModal: modal,
} as any;
const input = { initialize: vi.fn() } as any;
const transport = {
turnComplete: vi.fn(),
updateCallback: vi.fn(),
rejoinGame: vi.fn(),
leaveGame: vi.fn(),
} as any;
let workerCallback: any;
const worker = {
start: (cb: any) => {
workerCallback = cb;
},
sendHeartbeat: vi.fn(),
sendTurn: vi.fn(),
cleanup: vi.fn(),
} as any;
const myPlayer = {
cosmetics: {
pattern: { name: "purch_pattern", colorPalette: null },
},
troops: () => 1000,
clientID: () => "client123",
} as any;
const gameView = {
update: vi.fn(),
playerByClientID: vi.fn(() => myPlayer),
config: () => ({ isRandomSpawn: () => false }),
inSpawnPhase: () => false,
myPlayer: () => myPlayer,
} as any;
const lobby = {
clientID: "client123",
gameID: "purch_pattern",
playerName: "Tester",
cosmetics: {},
serverConfig: {} as any,
turnstileToken: null,
isSkinTest: true,
gameStartInfo: {
gameID: "purch_pattern",
players: [],
config: { isRandomSpawn: () => false },
lobbyCreatedAt: Date.now(),
},
} as any;
runner = new ClientGameRunner(
lobby,
"client123",
eventBus,
renderer,
input,
transport,
worker,
gameView,
{ playBackgroundMusic: vi.fn(), dispose: vi.fn() } as any,
{} as any,
);
runner.start();
expect(workerCallback).toBeTruthy();
const updates: any[] = [];
updates[GameUpdateType.Hash] = [];
updates[GameUpdateType.Win] = [
{
type: GameUpdateType.Win,
winner: ["player", "client123"],
allPlayersStats: {},
},
];
workerCallback({
tick: 1,
updates,
packedTileUpdates: new BigUint64Array(),
playerNameViewData: {},
tickExecutionDuration: 0,
});
// showByName() is async (fetchCosmetics + lit updates); wait for the
// microtask queue to drain, then for the next render.
await new Promise((r) => setTimeout(r, 0));
await modal.updateComplete;
expect(modal.isVisible).toBe(true);
// The mock cosmetic-button is also a custom element; let it render.
await new Promise((r) => setTimeout(r, 0));
const buyBtn = modal.querySelector(
'button[data-testid="buy-skin"]',
) as HTMLButtonElement | null;
expect(buyBtn).toBeTruthy();
buyBtn!.click();
expect(purchaseCosmeticMock).toHaveBeenCalledTimes(1);
expect(purchaseCosmeticMock.mock.calls[0][0].cosmetic.name).toBe(
"purch_pattern",
);
});
});