// Vitest coverage for the Phase 20 modernize cost preview. The // preview line in the inspector calls `core.blockUpgradeCost` once // per ship block and multiplies the per-ship total by the number of // targeted ships. The preview hides when `Core` is unavailable; when // `tech === "ALL"` the targets are the player's race tech levels; // otherwise only the picked block contributes to the cost. import "@testing-library/jest-dom/vitest"; import "fake-indexeddb/auto"; import { fireEvent, render } from "@testing-library/svelte"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { i18n } from "../src/lib/i18n/index.svelte"; import type { ReportLocalShipGroup, ReportPlanet, ShipClassSummary, } from "../src/api/game-state"; import ShipGroup, { type ShipGroupSelection, } from "../src/lib/inspectors/ship-group.svelte"; import { ORDER_DRAFT_CONTEXT_KEY, OrderDraftStore, } from "../src/sync/order-draft.svelte"; import { CORE_CONTEXT_KEY, CoreHolder } from "../src/lib/core-context.svelte"; import type { Core } from "../src/platform/core/index"; import { IDBCache } from "../src/platform/store/idb-cache"; import { openGalaxyDB, type GalaxyDB } from "../src/platform/store/idb"; import type { Cache } from "../src/platform/store/index"; import type { IDBPDatabase } from "idb"; const GAME_ID = "11111111-2222-3333-4444-555555555555"; let db: IDBPDatabase; let dbName: string; let cache: Cache; let draft: OrderDraftStore; const PLANETS: ReportPlanet[] = [ { number: 17, name: "Castle", x: 100, y: 100, kind: "local", owner: null, size: 1000, resources: 5, industryStockpile: 0, materialsStockpile: 0, industry: 1000, population: 1000, colonists: 0, production: "Capital", freeIndustry: 1000, }, ]; const SHIP_CLASS_CRUISER: ShipClassSummary = { name: "Cruiser", drive: 5, armament: 0, weapons: 0, shields: 5, cargo: 5, }; beforeEach(async () => { dbName = `galaxy-ship-group-modernize-${crypto.randomUUID()}`; db = await openGalaxyDB(dbName); cache = new IDBCache(db); draft = new OrderDraftStore(); await draft.init({ cache, gameId: GAME_ID }); i18n.resetForTests("en"); }); afterEach(async () => { draft.dispose(); db.close(); await new Promise((resolve) => { const req = indexedDB.deleteDatabase(dbName); req.onsuccess = () => resolve(); req.onerror = () => resolve(); req.onblocked = () => resolve(); }); }); function group( overrides: Partial = {}, ): ReportLocalShipGroup { return { id: "cccccccc-cccc-cccc-cccc-cccccccccccc", count: 4, class: "Cruiser", tech: { drive: 1, weapons: 0, shields: 1, cargo: 1 }, cargo: "NONE", load: 0, destination: 17, origin: null, range: null, speed: 0, mass: 25, state: "In_Orbit", fleet: null, ...overrides, }; } // stubCore mirrors `pkg/calc/ship.go.BlockUpgradeCost` exactly so the // preview line shows the same number the WASM bridge would produce. // The other Core methods are no-ops because the modernize preview // only consults `weaponsBlockMass` (returns null when armament is // zero) and `blockUpgradeCost`. function stubCore(): Core { return { signRequest: () => new Uint8Array(), verifyResponse: () => true, verifyEvent: () => true, verifyPayloadHash: () => true, driveEffective: ({ drive, driveTech }) => drive * driveTech, emptyMass: () => 0, weaponsBlockMass: ({ weapons, armament }) => { if ((armament === 0 && weapons !== 0) || (armament !== 0 && weapons === 0)) { return null; } return (armament + 1) * (weapons / 2); }, fullMass: ({ emptyMass, carryingMass }) => emptyMass + carryingMass, speed: () => 0, cargoCapacity: () => 0, carryingMass: () => 0, blockUpgradeCost: ({ blockMass, currentTech, targetTech }) => { if (blockMass === 0 || targetTech <= currentTech) return 0; return (1 - currentTech / targetTech) * 10 * blockMass; }, }; } function mount( g: ReportLocalShipGroup, options: { core?: Core | null } = {}, ) { const selection: ShipGroupSelection = { variant: "local", group: g }; const holder = new CoreHolder(); if (options.core !== undefined) holder.set(options.core); const context = new Map([ [ORDER_DRAFT_CONTEXT_KEY, draft], [CORE_CONTEXT_KEY, holder], ]); return render(ShipGroup, { props: { selection, planets: PLANETS, localShipClass: [SHIP_CLASS_CRUISER], localFleets: [], otherRaces: [], mapWidth: 1000, mapHeight: 1000, localPlayerDrive: 2, localPlayerWeapons: 2, localPlayerShields: 2, localPlayerCargo: 2, }, context, }); } describe("ship-group inspector — modernize cost preview", () => { test("ALL upgrade preview matches the BlockUpgradeCost formula × ship count", async () => { // drive: mass=5 current=1 target=2 → (1 - 0.5) * 10 * 5 = 25 // shields: mass=5 current=1 target=2 → 25 // cargo: mass=5 current=1 target=2 → 25 // weapons: armament=0 weapons=0 → block mass 0 → 0 // per-ship = 75; group of 4 → 300 const ui = mount(group(), { core: stubCore() }); await fireEvent.click(ui.getByTestId("inspector-ship-group-action-modernize")); const preview = ui.getByTestId("inspector-ship-group-form-modernize-cost"); expect(preview).toHaveTextContent("300"); }); test("per-block tech with custom level uses only that block", async () => { // DRIVE only, target=2: 25 per ship × 4 = 100. const ui = mount(group(), { core: stubCore() }); await fireEvent.click(ui.getByTestId("inspector-ship-group-action-modernize")); await fireEvent.change( ui.getByTestId("inspector-ship-group-form-modernize-tech"), { target: { value: "DRIVE" } }, ); await fireEvent.input( ui.getByTestId("inspector-ship-group-form-modernize-level"), { target: { value: "2" } }, ); const preview = ui.getByTestId("inspector-ship-group-form-modernize-cost"); expect(preview).toHaveTextContent("100"); }); test("preview is unavailable when Core is not loaded", async () => { const ui = mount(group(), { core: null }); await fireEvent.click(ui.getByTestId("inspector-ship-group-action-modernize")); const preview = ui.getByTestId("inspector-ship-group-form-modernize-cost"); expect(preview).toHaveTextContent(/preview unavailable/i); }); });