refactor: convert to npm-workspaces monorepo (engine/core-public/shared/client/server)

Restructure the single src/ tree into an npm-workspaces monorepo under
packages/, rename core -> engine, extract a types-only core-public layer,
and break the pre-existing engine -> client dependency cycle.

Structure (packages/):
  core-public  public API/wire schemas + shared enums (clean leaf)
  shared       framework-agnostic helpers (clean leaf)
  engine       deterministic simulation (was src/core)
  client       rendering/UI (was src/client)
  server       coordination (was src/server)

Dependency DAG: engine -> {core-public, shared}; client -> {core-public,
shared, engine}; server -> {core-public, engine}.

- npm workspaces: root package.json workspaces + per-package package.json;
  tsconfig.base.json holds shared options + path aliases
  (core-public/* shared/* engine/* client/* server/*) resolved uniformly by
  tsc, Vite (resolve.tsconfigPaths), Vitest, and tsx. Lockfile regenerated.
- core-public: moved Schemas/ApiSchemas/CosmeticSchemas/StatsSchemas/
  ClanApiSchemas/WorkerSchemas/Base64/PatternDecoder; extracted the enums
  (GameTypes), GameEvent type, emoji table, and GraphicsOverrides schema.
  Engine re-exports the moved enums/types so existing imports keep working.
- Broke engine -> client cycle:
  - renderNumber/renderTroops -> shared/format
  - NameBoxCalculator moved into engine
  - username validation returns translation key + params; client translates
  - applyStateUpdate moved to client (operates on the render-only PlayerState)
  - Config/UnitGrid/execution-Util/GameImpl now use structural read
    interfaces (engine/game/ReadViews: PlayerLike/UnitLike/GameLike) instead
    of importing client view classes; client imports view classes from a new
    client/view barrel; deleted the engine/game/GameView re-export shim.
- Build/deploy updated: vite.config, index.html, eslint, Dockerfile
  (copies packages/ + tsconfig.base.json before npm ci), .vscode, tests.

Verified: tsc --noEmit clean; 1364 + 65 tests pass; production vite build
succeeds; engine has zero client/server imports; core-public and shared are
dependency leaves.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Evan Pelle
2026-06-10 16:51:03 +00:00
co-authored by Claude Opus 4.8
parent cb9cab9aca
commit a7f992e9b0
674 changed files with 1637 additions and 1370 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
import { installSafariPinchZoomBlocker } from "../../src/client/utilities/DisableSafariPinchZoom";
import { installSafariPinchZoomBlocker } from "client/utilities/DisableSafariPinchZoom";
const GESTURE_EVENTS = ["gesturestart", "gesturechange", "gestureend"] as const;
+1 -1
View File
@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { JoinLobbyModal } from "../../src/client/JoinLobbyModal";
import { JoinLobbyModal } from "client/JoinLobbyModal";
describe("JoinLobbyModal server time offset", () => {
let nowMs = 0;
+7 -7
View File
@@ -7,7 +7,7 @@ vi.mock("@lit-labs/virtualizer/virtualize.js", async () => {
};
});
vi.mock("../../src/client/Utils", () => ({
vi.mock("client/Utils", () => ({
translateText: vi.fn((key: string) => {
const translations: Record<string, string> = {
"leaderboard_modal.win_score_tooltip":
@@ -39,7 +39,7 @@ vi.mock("../../src/client/Utils", () => ({
}),
}));
vi.mock("../../src/client/Api", () => {
vi.mock("client/Api", () => {
const getApiBase = () => "http://localhost:3000";
return {
getApiBase: vi.fn(getApiBase),
@@ -64,7 +64,7 @@ vi.mock("../../src/client/Api", () => {
};
});
vi.mock("../../src/client/ClanApi", () => {
vi.mock("client/ClanApi", () => {
const getApiBase = () => "http://localhost:3000";
return {
fetchClanLeaderboard: vi.fn(async () => {
@@ -101,8 +101,8 @@ beforeEach(() => {
);
});
import "../../src/client/components/baseComponents/Modal";
import { LeaderboardModal } from "../../src/client/LeaderboardModal";
import "client/components/baseComponents/Modal";
import { LeaderboardModal } from "client/LeaderboardModal";
describe("LeaderboardModal", () => {
let modal: LeaderboardModal;
@@ -210,7 +210,7 @@ describe("LeaderboardModal", () => {
it("should use translateText for tooltip internationalization", async () => {
// Verify translation keys are correct
const { translateText } = await import("../../src/client/Utils");
const { translateText } = await import("client/Utils");
expect(translateText("leaderboard_modal.win_score_tooltip")).toBe(
"Weighted wins based on clan participation and match difficulty",
@@ -223,7 +223,7 @@ describe("LeaderboardModal", () => {
describe("Player Data Mapping", () => {
it("should map ranked leaderboard data and set current user entry", async () => {
const { getUserMe } = await import("../../src/client/Api");
const { getUserMe } = await import("client/Api");
(getUserMe as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
player: { publicId: "player-2" },
});
+1 -1
View File
@@ -1,4 +1,4 @@
import { normalizeNewsMarkdown } from "../../src/client/NewsMarkdown";
import { normalizeNewsMarkdown } from "client/NewsMarkdown";
describe("normalizeNewsMarkdown", () => {
it("converts openfront pull request URLs to short markdown links", () => {
+1 -1
View File
@@ -25,7 +25,7 @@ const loadPlatform = async ({
userAgentData,
maxTouchPoints,
});
const { Platform } = await import("../../src/client/Platform");
const { Platform } = await import("client/Platform");
return Platform;
};
+3 -3
View File
@@ -1,10 +1,10 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../../../src/client/Api", () => ({
vi.mock("client/Api", () => ({
getApiBase: vi.fn(() => "http://localhost:3000"),
}));
vi.mock("../../../src/client/Auth", () => ({
vi.mock("client/Auth", () => ({
getAuthHeader: vi.fn(async () => "Bearer test-token"),
}));
@@ -12,7 +12,7 @@ import {
banClanMember,
fetchClanBans,
unbanClanMember,
} from "../../../src/client/ClanApi";
} from "client/ClanApi";
const okJson = (data: unknown, status = 200) => ({
ok: true,
+3 -3
View File
@@ -1,10 +1,10 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../../../src/client/Api", () => ({
vi.mock("client/Api", () => ({
getApiBase: vi.fn(() => "http://localhost:3000"),
}));
vi.mock("../../../src/client/Auth", () => ({
vi.mock("client/Auth", () => ({
getAuthHeader: vi.fn(async () => "Bearer test-token"),
}));
@@ -20,7 +20,7 @@ import {
transferLeadership,
updateClan,
withdrawClanRequest,
} from "../../../src/client/ClanApi";
} from "client/ClanApi";
const okJson = (data: unknown, status = 200) => ({
ok: true,
+5 -5
View File
@@ -1,15 +1,15 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../../../src/client/Api", () => ({
vi.mock("client/Api", () => ({
getApiBase: vi.fn(() => "http://localhost:3000"),
getUserMe: vi.fn(),
}));
vi.mock("../../../src/client/Auth", () => ({
vi.mock("client/Auth", () => ({
getAuthHeader: vi.fn(async () => "Bearer test-token"),
}));
import { getUserMe } from "../../../src/client/Api";
import { getUserMe } from "client/Api";
import {
checkClanTagOwnership,
fetchClanDetail,
@@ -19,8 +19,8 @@ import {
fetchClanMembers,
fetchClanRequests,
fetchClans,
} from "../../../src/client/ClanApi";
import type { UserMeResponse } from "../../../src/core/ApiSchemas";
} from "client/ClanApi";
import type { UserMeResponse } from "core-public/ApiSchemas";
const userWithClans = (tags: string[]): UserMeResponse =>
({
+1 -1
View File
@@ -9,7 +9,7 @@ import {
ClanInfoSchema,
ClanJoinRequestSchema,
ClanMemberSchema,
} from "../../../src/core/ClanApiSchemas";
} from "core-public/ClanApiSchemas";
describe("ClanInfoSchema", () => {
const base = {
+10 -10
View File
@@ -4,7 +4,7 @@ import { flushAsync } from "./ClanModalTestUtils";
// ─── Mocks (defined before imports so vi.mock hoisting applies) ─────────────
vi.mock("../../../src/client/Utils", () => ({
vi.mock("client/Utils", () => ({
// Echo the key so we can assert on translation slugs.
translateText: vi.fn((key: string) => key),
showToast: vi.fn(),
@@ -13,22 +13,22 @@ vi.mock("../../../src/client/Utils", () => ({
getMapName: vi.fn((m: string | undefined) => m ?? null),
}));
vi.mock("../../../src/client/Auth", () => ({
vi.mock("client/Auth", () => ({
getAuthHeader: vi.fn(async () => "Bearer test-token"),
userAuth: vi.fn(),
}));
vi.mock("../../../src/client/ClanApi", () => ({
vi.mock("client/ClanApi", () => ({
fetchClanGames: vi.fn(async () => ({ results: [], nextCursor: null })),
}));
vi.mock("../../../src/client/TerrainMapFileLoader", () => ({
vi.mock("client/TerrainMapFileLoader", () => ({
terrainMapFileLoader: {
getMapData: vi.fn(() => ({ webpPath: "/maps/test.webp" })),
},
}));
vi.mock("../../../src/client/ClientEnv", () => ({
vi.mock("client/ClientEnv", () => ({
ClientEnv: {
workerPath: vi.fn(() => "w0"),
},
@@ -36,7 +36,7 @@ vi.mock("../../../src/client/ClientEnv", () => ({
// ClanShared re-exports from BaseModal; stub it directly so we don't pull
// BaseModal's dependency graph into this unit test.
vi.mock("../../../src/client/components/clan/ClanShared", async () => {
vi.mock("client/components/clan/ClanShared", async () => {
const { html } = await import("lit");
return {
renderLoadingSpinner: vi.fn(() => html`<div data-testid="spinner"></div>`),
@@ -46,7 +46,7 @@ vi.mock("../../../src/client/components/clan/ClanShared", async () => {
// CopyButton is a custom element; stub it so its dependency graph (Auth,
// API, etc.) doesn't get pulled in transitively.
vi.mock("../../../src/client/components/CopyButton", () => ({}));
vi.mock("client/components/CopyButton", () => ({}));
// jsdom doesn't ship IntersectionObserver — provide the minimum surface
// the component touches (observe / disconnect). Tests below trigger the
@@ -75,12 +75,12 @@ vi.stubGlobal("IntersectionObserver", FakeIntersectionObserver);
// ─── Imports under test ──────────────────────────────────────────────────────
import type { ClanGame, ClanGamesResponse } from "../../../src/client/ClanApi";
import { fetchClanGames } from "../../../src/client/ClanApi";
import type { ClanGame, ClanGamesResponse } from "client/ClanApi";
import { fetchClanGames } from "client/ClanApi";
import {
ClanGameHistoryView,
type ClanGameHistoryCache,
} from "../../../src/client/components/clan/ClanGameHistoryView";
} from "client/components/clan/ClanGameHistoryView";
// ─── Helpers ────────────────────────────────────────────────────────────────
+33 -33
View File
@@ -17,16 +17,16 @@ import {
} from "./ClanModalTestUtils";
vi.mock("@lit-labs/virtualizer/virtualize.js", () => virtualizerMockFactory());
vi.mock("../../../src/client/Api", () => apiMockFactory());
vi.mock("../../../src/client/ClanApi", () => clanApiMockFactory());
vi.mock("../../../src/client/Utils", () => utilsMockFactory());
vi.mock("../../../src/client/Auth", () => authMockFactory());
vi.mock("../../../src/client/CrazyGamesSDK", () => crazyGamesSdkMockFactory());
vi.mock("client/Api", () => apiMockFactory());
vi.mock("client/ClanApi", () => clanApiMockFactory());
vi.mock("client/Utils", () => utilsMockFactory());
vi.mock("client/Auth", () => authMockFactory());
vi.mock("client/CrazyGamesSDK", () => crazyGamesSdkMockFactory());
stubLocalStorage();
import type { ClanInfo } from "../../../src/client/ClanApi";
import { ClanModal } from "../../../src/client/ClanModal";
import type { ClanInfo } from "client/ClanApi";
import { ClanModal } from "client/ClanModal";
describe("ClanModal — handlers", () => {
let modal: ClanModal;
@@ -50,7 +50,7 @@ describe("ClanModal — handlers", () => {
describe("handleApprove increments selectedClan.memberCount", () => {
it("increments memberCount by 1 after successful approveClanRequest", async () => {
const { approveClanRequest, fetchClanRequests } =
await import("../../../src/client/ClanApi");
await import("client/ClanApi");
(approveClanRequest as ReturnType<typeof vi.fn>).mockResolvedValue(true);
(fetchClanRequests as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
results: [
@@ -86,7 +86,7 @@ describe("ClanModal — handlers", () => {
it("does not increment memberCount when approveClanRequest fails", async () => {
const { approveClanRequest, fetchClanRequests } =
await import("../../../src/client/ClanApi");
await import("client/ClanApi");
(approveClanRequest as ReturnType<typeof vi.fn>).mockResolvedValue({
error: "clan_modal.error_generic",
});
@@ -120,7 +120,7 @@ describe("ClanModal — handlers", () => {
it("treats undefined memberCount as 0 and increments to 1", async () => {
const { approveClanRequest, fetchClanRequests } =
await import("../../../src/client/ClanApi");
await import("client/ClanApi");
(approveClanRequest as ReturnType<typeof vi.fn>).mockResolvedValue(true);
(fetchClanRequests as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
results: [
@@ -154,7 +154,7 @@ describe("ClanModal — handlers", () => {
let manageView: Element;
beforeEach(async () => {
const { fetchClanMembers } = await import("../../../src/client/ClanApi");
const { fetchClanMembers } = await import("client/ClanApi");
(fetchClanMembers as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
results: [
{
@@ -188,7 +188,7 @@ describe("ClanModal — handlers", () => {
});
it("handleBan calls banClanMember after confirm-dialog confirm", async () => {
const { banClanMember } = await import("../../../src/client/ClanApi");
const { banClanMember } = await import("client/ClanApi");
(banClanMember as ReturnType<typeof vi.fn>).mockResolvedValue(true);
// Step 1: Click Ban button to open confirm dialog
@@ -216,7 +216,7 @@ describe("ClanModal — handlers", () => {
});
it("handleBan aborts when confirm-dialog cancel is clicked", async () => {
const { banClanMember } = await import("../../../src/client/ClanApi");
const { banClanMember } = await import("client/ClanApi");
// Step 1: Click Ban button to open confirm dialog
const banButton = Array.from(modal.querySelectorAll("button")).find(
@@ -237,7 +237,7 @@ describe("ClanModal — handlers", () => {
});
it("handleBan sends undefined reason when confirm text is empty", async () => {
const { banClanMember } = await import("../../../src/client/ClanApi");
const { banClanMember } = await import("client/ClanApi");
(banClanMember as ReturnType<typeof vi.fn>).mockResolvedValue(true);
// Step 1: Click Ban button
@@ -265,7 +265,7 @@ describe("ClanModal — handlers", () => {
it("handleBan syncs memberCount via clan-updated event on success", async () => {
const { banClanMember, fetchClanMembers } =
await import("../../../src/client/ClanApi");
await import("client/ClanApi");
(banClanMember as ReturnType<typeof vi.fn>).mockResolvedValue(true);
// Server returns the post-ban member total (was 5, now 4).
(fetchClanMembers as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
@@ -303,7 +303,7 @@ describe("ClanModal — handlers", () => {
describe("handleUnban", () => {
it("removes ban from list and decrements total on success", async () => {
const { unbanClanMember, fetchClanBans } =
await import("../../../src/client/ClanApi");
await import("client/ClanApi");
(unbanClanMember as ReturnType<typeof vi.fn>).mockResolvedValue(true);
(fetchClanBans as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
results: [
@@ -341,7 +341,7 @@ describe("ClanModal — handlers", () => {
let manageView: Element;
beforeEach(async () => {
const { fetchClanMembers } = await import("../../../src/client/ClanApi");
const { fetchClanMembers } = await import("client/ClanApi");
(fetchClanMembers as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
results: [
{
@@ -369,7 +369,7 @@ describe("ClanModal — handlers", () => {
it("calls kickMember and syncs memberCount on success", async () => {
const { kickMember, fetchClanMembers } =
await import("../../../src/client/ClanApi");
await import("client/ClanApi");
(kickMember as ReturnType<typeof vi.fn>).mockResolvedValue(true);
(fetchClanMembers as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
results: [],
@@ -402,7 +402,7 @@ describe("ClanModal — handlers", () => {
it("does not mutate state when kickMember fails", async () => {
const { kickMember, fetchClanMembers } =
await import("../../../src/client/ClanApi");
await import("client/ClanApi");
(kickMember as ReturnType<typeof vi.fn>).mockResolvedValue({
error: "clan_modal.error_generic",
});
@@ -436,7 +436,7 @@ describe("ClanModal — handlers", () => {
let manageView: Element;
beforeEach(async () => {
const { fetchClanMembers } = await import("../../../src/client/ClanApi");
const { fetchClanMembers } = await import("client/ClanApi");
(fetchClanMembers as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
results: [],
total: 3,
@@ -467,7 +467,7 @@ describe("ClanModal — handlers", () => {
});
it("calls disbandClan, clears selection, and returns to list on success", async () => {
const { disbandClan } = await import("../../../src/client/ClanApi");
const { disbandClan } = await import("client/ClanApi");
(disbandClan as ReturnType<typeof vi.fn>).mockResolvedValue(true);
// Open the disband confirm dialog on the manage view.
@@ -495,7 +495,7 @@ describe("ClanModal — handlers", () => {
});
it("preserves selection when disbandClan fails", async () => {
const { disbandClan } = await import("../../../src/client/ClanApi");
const { disbandClan } = await import("client/ClanApi");
(disbandClan as ReturnType<typeof vi.fn>).mockResolvedValue({
error: "clan_modal.error_generic",
});
@@ -525,7 +525,7 @@ describe("ClanModal — handlers", () => {
let requestsView: Element;
beforeEach(async () => {
const { fetchClanRequests } = await import("../../../src/client/ClanApi");
const { fetchClanRequests } = await import("client/ClanApi");
(fetchClanRequests as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
results: [
{ publicId: "applicant-1", createdAt: "2024-06-01T00:00:00Z" },
@@ -543,7 +543,7 @@ describe("ClanModal — handlers", () => {
});
it("removes the request and decrements totals on success", async () => {
const { denyClanRequest } = await import("../../../src/client/ClanApi");
const { denyClanRequest } = await import("client/ClanApi");
(denyClanRequest as ReturnType<typeof vi.fn>).mockResolvedValue(true);
const denyButton = Array.from(modal.querySelectorAll("button")).find(
@@ -564,7 +564,7 @@ describe("ClanModal — handlers", () => {
});
it("does not mutate state when denyClanRequest fails", async () => {
const { denyClanRequest } = await import("../../../src/client/ClanApi");
const { denyClanRequest } = await import("client/ClanApi");
(denyClanRequest as ReturnType<typeof vi.fn>).mockResolvedValue({
error: "clan_modal.error_generic",
});
@@ -589,7 +589,7 @@ describe("ClanModal — handlers", () => {
describe("handleJoin", () => {
beforeEach(async () => {
const { fetchClanDetail } = await import("../../../src/client/ClanApi");
const { fetchClanDetail } = await import("client/ClanApi");
(fetchClanDetail as ReturnType<typeof vi.fn>).mockResolvedValueOnce(
makeClan({ isOpen: true, memberCount: 5 }),
);
@@ -602,7 +602,7 @@ describe("ClanModal — handlers", () => {
it("switches detail view into member mode immediately after open-clan join", async () => {
const { joinClan, fetchClanMembers } =
await import("../../../src/client/ClanApi");
await import("client/ClanApi");
(joinClan as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
status: "joined",
});
@@ -651,7 +651,7 @@ describe("ClanModal — handlers", () => {
describe("handleLeave", () => {
beforeEach(async () => {
const { fetchClanDetail, fetchClanMembers } =
await import("../../../src/client/ClanApi");
await import("client/ClanApi");
(fetchClanDetail as ReturnType<typeof vi.fn>).mockResolvedValueOnce(
makeClan(),
);
@@ -680,7 +680,7 @@ describe("ClanModal — handlers", () => {
});
it("calls leaveClan, removes role, and returns to list on success", async () => {
const { leaveClan } = await import("../../../src/client/ClanApi");
const { leaveClan } = await import("client/ClanApi");
(leaveClan as ReturnType<typeof vi.fn>).mockResolvedValue(true);
const leaveButton = Array.from(modal.querySelectorAll("button")).find(
@@ -704,7 +704,7 @@ describe("ClanModal — handlers", () => {
});
it("preserves selection when leaveClan fails", async () => {
const { leaveClan } = await import("../../../src/client/ClanApi");
const { leaveClan } = await import("client/ClanApi");
(leaveClan as ReturnType<typeof vi.fn>).mockResolvedValue({
error: "clan_modal.error_generic",
});
@@ -732,7 +732,7 @@ describe("ClanModal — handlers", () => {
let transferView: Element;
beforeEach(async () => {
const { fetchClanMembers } = await import("../../../src/client/ClanApi");
const { fetchClanMembers } = await import("client/ClanApi");
(fetchClanMembers as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
results: [
{
@@ -766,7 +766,7 @@ describe("ClanModal — handlers", () => {
it("clears confirmAction and removes the dialog after confirming", async () => {
const { transferLeadership } =
await import("../../../src/client/ClanApi");
await import("client/ClanApi");
(transferLeadership as ReturnType<typeof vi.fn>).mockResolvedValue(true);
const dialog = modal.querySelector("confirm-dialog");
@@ -786,7 +786,7 @@ describe("ClanModal — handlers", () => {
it("clears confirmAction when cancel is clicked, without calling the API", async () => {
const { transferLeadership } =
await import("../../../src/client/ClanApi");
await import("client/ClanApi");
const dialog = modal.querySelector("confirm-dialog");
expect(dialog).toBeTruthy();
+17 -17
View File
@@ -15,15 +15,15 @@ import {
} from "./ClanModalTestUtils";
vi.mock("@lit-labs/virtualizer/virtualize.js", () => virtualizerMockFactory());
vi.mock("../../../src/client/Api", () => apiMockFactory());
vi.mock("../../../src/client/ClanApi", () => clanApiMockFactory());
vi.mock("../../../src/client/Utils", () => utilsMockFactory());
vi.mock("../../../src/client/Auth", () => authMockFactory());
vi.mock("../../../src/client/CrazyGamesSDK", () => crazyGamesSdkMockFactory());
vi.mock("client/Api", () => apiMockFactory());
vi.mock("client/ClanApi", () => clanApiMockFactory());
vi.mock("client/Utils", () => utilsMockFactory());
vi.mock("client/Auth", () => authMockFactory());
vi.mock("client/CrazyGamesSDK", () => crazyGamesSdkMockFactory());
stubLocalStorage();
import { ClanModal } from "../../../src/client/ClanModal";
import { ClanModal } from "client/ClanModal";
describe("ClanModal — rendering", () => {
let modal: ClanModal;
@@ -51,7 +51,7 @@ describe("ClanModal — rendering", () => {
// Directly invoke renderClanCard via the instance and insert the result
// into a container so we can query it. We do this by populating myClans
// and myClanRoles state so the list view renders real cards.
const { getUserMe } = await import("../../../src/client/Api");
const { getUserMe } = await import("client/Api");
(getUserMe as ReturnType<typeof vi.fn>).mockResolvedValue({
player: {
publicId: "test-player",
@@ -88,7 +88,7 @@ describe("ClanModal — rendering", () => {
});
it("shows 'clan_modal.open' badge when clan is open and user has no role", async () => {
const { fetchClans } = await import("../../../src/client/ClanApi");
const { fetchClans } = await import("client/ClanApi");
(fetchClans as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
results: [makeClan({ tag: "OTH", name: "Other Clan", isOpen: true })],
total: 1,
@@ -107,7 +107,7 @@ describe("ClanModal — rendering", () => {
});
it("shows 'clan_modal.invite_only' badge when clan is closed and user has no role", async () => {
const { fetchClans } = await import("../../../src/client/ClanApi");
const { fetchClans } = await import("client/ClanApi");
(fetchClans as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
results: [makeClan({ tag: "INV", name: "Invite Clan", isOpen: false })],
total: 1,
@@ -187,7 +187,7 @@ describe("ClanModal — rendering", () => {
});
it("does NOT show a role badge when myClanRoles has no entry for the clan", async () => {
const { fetchClans } = await import("../../../src/client/ClanApi");
const { fetchClans } = await import("client/ClanApi");
(fetchClans as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
results: [makeClan({ tag: "INV", name: "Invite Clan", isOpen: false })],
total: 1,
@@ -227,7 +227,7 @@ describe("ClanModal — rendering", () => {
expect(modal.textContent).not.toContain("undefined");
// translateText mock swallows args and returns the key, so verify it
// was called with count: 0 (the fallback) rather than count: undefined.
const { translateText } = await import("../../../src/client/Utils");
const { translateText } = await import("client/Utils");
const calls = (translateText as ReturnType<typeof vi.fn>).mock.calls;
const memberCountCall = calls.find(
(c) => c[0] === "clan_modal.member_count",
@@ -237,7 +237,7 @@ describe("ClanModal — rendering", () => {
});
it("shows 0 in the stats row of the detail view when memberCount is undefined", async () => {
const { fetchClanDetail } = await import("../../../src/client/ClanApi");
const { fetchClanDetail } = await import("client/ClanApi");
(fetchClanDetail as ReturnType<typeof vi.fn>).mockResolvedValueOnce(
makeClan({ memberCount: undefined }),
);
@@ -251,7 +251,7 @@ describe("ClanModal — rendering", () => {
});
it("shows 0 in the manage members header when memberCount is undefined", async () => {
const { fetchClanMembers } = await import("../../../src/client/ClanApi");
const { fetchClanMembers } = await import("client/ClanApi");
(fetchClanMembers as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
results: [],
total: 0,
@@ -277,7 +277,7 @@ describe("ClanModal — rendering", () => {
describe("Open/Closed toggle ARIA attributes in manage view", () => {
beforeEach(async () => {
const { fetchClanMembers } = await import("../../../src/client/ClanApi");
const { fetchClanMembers } = await import("client/ClanApi");
(fetchClanMembers as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
results: [],
total: 0,
@@ -355,7 +355,7 @@ describe("ClanModal — rendering", () => {
describe("Ban feature — bans view", () => {
it("renders Banned Players button in manage view", async () => {
const { fetchClanMembers } = await import("../../../src/client/ClanApi");
const { fetchClanMembers } = await import("client/ClanApi");
(fetchClanMembers as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
results: [],
total: 0,
@@ -374,7 +374,7 @@ describe("ClanModal — rendering", () => {
});
it("renders ban list with unban button in bans view", async () => {
const { fetchClanBans } = await import("../../../src/client/ClanApi");
const { fetchClanBans } = await import("client/ClanApi");
(fetchClanBans as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
results: [
{
@@ -400,7 +400,7 @@ describe("ClanModal — rendering", () => {
});
it("renders empty state when no bans", async () => {
const { fetchClanBans } = await import("../../../src/client/ClanApi");
const { fetchClanBans } = await import("client/ClanApi");
(fetchClanBans as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
results: [],
total: 0,
+2 -2
View File
@@ -1,6 +1,6 @@
import { vi } from "vitest";
import type { ClanInfo } from "../../../src/client/ClanApi";
import type { ClanModal } from "../../../src/client/ClanModal";
import type { ClanInfo } from "client/ClanApi";
import type { ClanModal } from "client/ClanModal";
// ─── Mock factories ─────────────────────────────────────────────────────────
// Each factory returns a fresh object of vi.fn()s. Test files pass these to
+2 -2
View File
@@ -4,12 +4,12 @@ import type {
ClanJoinRequest,
ClanMember,
ClanMemberStats,
} from "../../../src/client/ClanApi";
} from "client/ClanApi";
import {
filterMembersBySearch,
filterRequestsBySearch,
renderMemberStats,
} from "../../../src/client/components/clan/ClanShared";
} from "client/components/clan/ClanShared";
const members: ClanMember[] = [
{ publicId: "Alice123", role: "leader", joinedAt: "2024-01-01T00:00:00Z" },
+2 -2
View File
@@ -1,7 +1,7 @@
import { FluentSlider } from "../../../src/client/components/FluentSlider";
import { FluentSlider } from "client/components/FluentSlider";
// Mock the translateText function
vi.mock("../../../src/client/Utils", () => ({
vi.mock("client/Utils", () => ({
translateText: vi.fn((key: string) => key),
}));
+2 -2
View File
@@ -1,8 +1,8 @@
import newsItems from "../../../resources/news.json";
import newsItems from "resources/news.json";
import {
getVisibleNewsItems,
NewsItem,
} from "../../../src/client/components/NewsBox";
} from "client/components/NewsBox";
const DISMISSED_NEWS_KEY = "dismissedNewsItems";
const allItems = newsItems as NewsItem[];
@@ -1,6 +1,6 @@
import { describe, expect, test } from "vitest";
import { shouldPreserveGhostAfterBuild } from "../../../src/client/controllers/BuildPreviewController";
import { UnitType } from "../../../src/core/game/Game";
import { shouldPreserveGhostAfterBuild } from "client/controllers/BuildPreviewController";
import { UnitType } from "engine/game/Game";
describe("BuildPreviewController ghost preservation (locked nuke / Enter confirm)", () => {
describe("shouldPreserveGhostAfterBuild", () => {
@@ -1,5 +1,5 @@
import { WarshipSelectionController } from "../../../src/client/controllers/WarshipSelectionController";
import { UnitSelectionEvent } from "../../../src/client/InputHandler";
import { WarshipSelectionController } from "client/controllers/WarshipSelectionController";
import { UnitSelectionEvent } from "client/InputHandler";
describe("WarshipSelectionController", () => {
let game: any;
@@ -6,20 +6,20 @@ import {
MenuElementParams,
rootMenuElement,
Slot,
} from "../../../src/client/hud/layers/RadialMenuElements";
import { UnitType } from "../../../src/core/game/Game";
import { TileRef } from "../../../src/core/game/GameMap";
import { GameView, PlayerView } from "../../../src/core/game/GameView";
} from "client/hud/layers/RadialMenuElements";
import { UnitType } from "engine/game/Game";
import { TileRef } from "engine/game/GameMap";
import { GameView, PlayerView } from "client/view";
vi.mock("../../../src/client/Utils", () => ({
vi.mock("client/Utils", () => ({
translateText: vi.fn((key: string) => key),
renderNumber: vi.fn((num: number) => num.toString()),
}));
vi.mock("../../../src/client/hud/layers/BuildMenu", async () => {
vi.mock("client/hud/layers/BuildMenu", async () => {
const { UnitType } = await vi.importActual<
typeof import("../../../src/core/game/Game")
>("../../../src/core/game/Game");
typeof import("engine/game/Game")
>("engine/game/Game");
return {
flattenedBuildTable: [
{
@@ -600,8 +600,8 @@ describe("RadialMenuElements", () => {
describe("Translation integration", () => {
it("should use translateText for tooltip items in build menu", async () => {
const { translateText } = await vi.importMock<
typeof import("../../../src/client/Utils")
>("../../../src/client/Utils");
typeof import("client/Utils")
>("client/Utils");
(translateText as Mock).mockClear();
@@ -615,8 +615,8 @@ describe("RadialMenuElements", () => {
it("should use translateText for tooltip items in attack menu", async () => {
const { translateText } = await vi.importMock<
typeof import("../../../src/client/Utils")
>("../../../src/client/Utils");
typeof import("client/Utils")
>("client/Utils");
(translateText as Mock).mockClear();
@@ -1,4 +1,4 @@
import { GameUpdateType } from "../../../../src/core/game/GameUpdates";
import { GameUpdateType } from "engine/game/GameUpdates";
vi.mock("lit", () => ({
html: () => {},
@@ -21,8 +21,8 @@ vi.mock("lit/directives/unsafe-html.js", () => ({
UnsafeHTMLDirective: class {},
}));
import { ActionableEvents } from "../../../../src/client/hud/layers/ActionableEvents";
import { MessageType } from "../../../../src/core/game/Game";
import { ActionableEvents } from "client/hud/layers/ActionableEvents";
import { MessageType } from "engine/game/Game";
describe("ActionableEvents - alliance renewal cleanup (allianceID based)", () => {
function makeRenewal(
@@ -2,8 +2,8 @@ import { describe, expect, test } from "vitest";
import {
alignClusterOrder,
Slot,
} from "../../../../src/client/controllers/AttackingTroopsController";
import { Cell } from "../../../../src/core/game/Game";
} from "client/controllers/AttackingTroopsController";
import { Cell } from "engine/game/Game";
// Slots only need the `dst` fields populated for `alignClusterOrder` — it
// compares the new positions against the previous targets to decide whether
@@ -15,23 +15,23 @@ vi.mock("lit/decorators.js", () => ({
query: () => () => {},
}));
vi.mock("../../../../src/client/Utils", () => ({
vi.mock("client/Utils", () => ({
translateText: vi.fn((key: string) => key),
renderDuration: vi.fn(),
renderNumber: vi.fn(),
renderTroops: vi.fn(),
}));
vi.mock("../../../../src/client/components/ui/ActionButton", () => ({
vi.mock("client/components/ui/ActionButton", () => ({
actionButton: vi.fn((props: unknown) => props),
}));
import { actionButton } from "../../../../src/client/components/ui/ActionButton";
import { PlayerModerationModal } from "../../../../src/client/hud/layers/PlayerModerationModal";
import { PlayerPanel } from "../../../../src/client/hud/layers/PlayerPanel";
import { SendKickPlayerIntentEvent } from "../../../../src/client/Transport";
import { PlayerType } from "../../../../src/core/game/Game";
import { PlayerView } from "../../../../src/core/game/GameView";
import { actionButton } from "client/components/ui/ActionButton";
import { PlayerModerationModal } from "client/hud/layers/PlayerModerationModal";
import { PlayerPanel } from "client/hud/layers/PlayerPanel";
import { SendKickPlayerIntentEvent } from "client/Transport";
import { PlayerType } from "engine/game/Game";
import { PlayerView } from "client/view";
describe("PlayerPanel - kick player moderation", () => {
let panel: PlayerPanel;
@@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { RankedType } from "../../../../src/core/game/Game";
import { RankedType } from "engine/game/Game";
vi.mock("../../../../src/client/Utils", () => ({
vi.mock("client/Utils", () => ({
translateText: vi.fn((key: string) => {
const translations: Record<string, string> = {
"win_modal.exit": "Exit",
@@ -16,17 +16,17 @@ vi.mock("../../../../src/client/Utils", () => ({
TUTORIAL_VIDEO_URL: "https://example.com/tutorial",
}));
vi.mock("../../../../src/client/Api", () => ({
vi.mock("client/Api", () => ({
getUserMe: vi.fn(async () => null),
}));
vi.mock("../../../../src/client/Cosmetics", () => ({
vi.mock("client/Cosmetics", () => ({
fetchCosmetics: vi.fn(async () => []),
handlePurchase: vi.fn(),
patternRelationship: vi.fn(() => ({})),
}));
vi.mock("../../../../src/client/CrazyGamesSDK", () => ({
vi.mock("client/CrazyGamesSDK", () => ({
crazyGamesSDK: {
happytime: vi.fn(),
requestAd: vi.fn(),
@@ -12,15 +12,15 @@
*/
import { describe, expect, it } from "vitest";
import { computePlayerStatus } from "../../../../../src/client/render/frame/derive/PlayerStatus";
import { computePlayerStatus } from "client/render/frame/derive/PlayerStatus";
import type {
PlayerState,
UnitState,
} from "../../../../../src/client/render/types";
} from "client/render/types";
import {
UT_ATOM_BOMB,
UT_WARSHIP,
} from "../../../../../src/client/render/types";
} from "client/render/types";
function ps(overrides: Partial<PlayerState> = {}): PlayerState {
return {
+6 -6
View File
@@ -40,9 +40,9 @@ vi.mock("howler", () => {
});
// Mock the Sounds module so tests don't depend on actual asset paths
vi.mock("../../../src/client/sound/Sounds", async (importOriginal) => {
vi.mock("client/sound/Sounds", async (importOriginal) => {
const actual =
await importOriginal<typeof import("../../../src/client/sound/Sounds")>();
await importOriginal<typeof import("client/sound/Sounds")>();
return {
...actual,
soundEffectUrls: new Map([
@@ -62,14 +62,14 @@ vi.mock("../../../src/client/sound/Sounds", async (importOriginal) => {
import {
MAX_CONCURRENT_SOUNDS,
SoundManager,
} from "../../../src/client/sound/SoundManager";
} from "client/sound/SoundManager";
import {
PlaySoundEffectEvent,
SetBackgroundMusicVolumeEvent,
SetSoundEffectsVolumeEvent,
} from "../../../src/client/sound/Sounds";
import { EventBus } from "../../../src/core/EventBus";
import { UserSettings } from "../../../src/core/game/UserSettings";
} from "client/sound/Sounds";
import { EventBus } from "engine/EventBus";
import { UserSettings } from "engine/game/UserSettings";
function createUserSettings(musicVolume = 0, sfxVolume = 1): UserSettings {
const settings = new UserSettings();
+2 -2
View File
@@ -10,8 +10,8 @@
*/
import { describe, expect, it } from "vitest";
import { UnitType } from "../../../src/core/game/Game";
import { GameUpdateType } from "../../../src/core/game/GameUpdates";
import { UnitType } from "engine/game/Game";
import { GameUpdateType } from "engine/game/GameUpdates";
import {
makeEmptyGu,
makeGameView,
+3 -3
View File
@@ -7,9 +7,9 @@
*/
import { describe, expect, it } from "vitest";
import { PlayerView } from "../../../src/client/view/PlayerView";
import { PlayerType } from "../../../src/core/game/Game";
import { GameUpdateType } from "../../../src/core/game/GameUpdates";
import { PlayerView } from "client/view/PlayerView";
import { PlayerType } from "engine/game/Game";
import { GameUpdateType } from "engine/game/GameUpdates";
import {
makeEmptyGu,
makeGameView,
+2 -2
View File
@@ -6,13 +6,13 @@
*/
import { describe, expect, it } from "vitest";
import { UnitView } from "../../../src/client/view/UnitView";
import { UnitView } from "client/view/UnitView";
import {
TrainType,
TransportShipState,
UnitType,
WarshipState,
} from "../../../src/core/game/Game";
} from "engine/game/Game";
import { makeGameView, makeUnitUpdate, stubConfig } from "../../util/viewStubs";
describe("UnitView accessors", () => {