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
+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" },