mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-10 17:04:35 +00:00
a7f992e9b0
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>
93 lines
2.8 KiB
TypeScript
93 lines
2.8 KiB
TypeScript
import newsItems from "resources/news.json";
|
|
import {
|
|
getVisibleNewsItems,
|
|
NewsItem,
|
|
} from "client/components/NewsBox";
|
|
|
|
const DISMISSED_NEWS_KEY = "dismissedNewsItems";
|
|
const allItems = newsItems as NewsItem[];
|
|
|
|
function createMockLocalStorage(): Storage {
|
|
let store: Record<string, string> = {};
|
|
return {
|
|
getItem: (key: string) => store[key] ?? null,
|
|
setItem: (key: string, value: string) => {
|
|
store[key] = String(value);
|
|
},
|
|
removeItem: (key: string) => {
|
|
delete store[key];
|
|
},
|
|
clear: () => {
|
|
store = {};
|
|
},
|
|
get length() {
|
|
return Object.keys(store).length;
|
|
},
|
|
key: (index: number) => Object.keys(store)[index] ?? null,
|
|
};
|
|
}
|
|
|
|
describe("NewsBox", () => {
|
|
beforeEach(() => {
|
|
vi.stubGlobal("localStorage", createMockLocalStorage());
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
describe("getVisibleNewsItems", () => {
|
|
it("returns all items when none are dismissed", () => {
|
|
const items = getVisibleNewsItems(allItems);
|
|
expect(items.length).toBe(newsItems.length);
|
|
});
|
|
|
|
it("filters out dismissed items", () => {
|
|
const items = getVisibleNewsItems(allItems);
|
|
const firstId = items[0].id;
|
|
localStorage.setItem(DISMISSED_NEWS_KEY, JSON.stringify([firstId]));
|
|
const filtered = getVisibleNewsItems(allItems);
|
|
expect(filtered.find((i) => i.id === firstId)).toBeUndefined();
|
|
expect(filtered.length).toBe(items.length - 1);
|
|
});
|
|
|
|
it("returns empty when all items are dismissed", () => {
|
|
const allIds = allItems.map((i) => i.id);
|
|
localStorage.setItem(DISMISSED_NEWS_KEY, JSON.stringify(allIds));
|
|
const items = getVisibleNewsItems(allItems);
|
|
expect(items.length).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("news items structure", () => {
|
|
it("each item has required fields", () => {
|
|
const items = getVisibleNewsItems(allItems);
|
|
for (const item of items) {
|
|
expect(item.id).toBeDefined();
|
|
expect(typeof item.id).toBe("string");
|
|
expect(item.title).toBeDefined();
|
|
expect(typeof item.title).toBe("string");
|
|
const hasDescription =
|
|
item.description !== undefined ||
|
|
item.descriptionTranslationKey !== undefined;
|
|
expect(hasDescription).toBe(true);
|
|
expect(item.type).toBeDefined();
|
|
expect(["tournament", "tutorial", "announcement", "warning"]).toContain(
|
|
item.type,
|
|
);
|
|
}
|
|
});
|
|
|
|
it("each item has a unique id", () => {
|
|
const items = getVisibleNewsItems(allItems);
|
|
const ids = items.map((i) => i.id);
|
|
expect(new Set(ids).size).toBe(ids.length);
|
|
});
|
|
|
|
it("contains a tournament entry", () => {
|
|
const items = getVisibleNewsItems(allItems);
|
|
expect(items.some((i) => i.type === "tournament")).toBe(true);
|
|
});
|
|
});
|
|
});
|