diff --git a/ui/frontend/src/lib/active-view/table-races.svelte b/ui/frontend/src/lib/active-view/table-races.svelte index 5446e1f..73d3f43 100644 --- a/ui/frontend/src/lib/active-view/table-races.svelte +++ b/ui/frontend/src/lib/active-view/table-races.svelte @@ -148,6 +148,15 @@ data fetching is performed here — the layout is responsible. async function setStance(acceptor: string, relation: Relation): Promise { if (draft === undefined) return; + // No-op when the row already reflects the requested stance — the + // engine would accept the duplicate, but queueing a wire entry + // that re-states the current state inflates the order tab and + // the auto-sync envelope for nothing. The current stance reads + // off the overlay (`races[i].relation`), so a queued-but-not- + // applied stance change correctly suppresses a duplicate click + // that matches the *queued* intent, not just the server snapshot. + const current = races.find((r) => r.name === acceptor)?.relation; + if (current === relation) return; await draft.add({ kind: "setDiplomaticStance", id: crypto.randomUUID(), diff --git a/ui/frontend/tests/table-races.test.ts b/ui/frontend/tests/table-races.test.ts index e6b5291..c07de22 100644 --- a/ui/frontend/tests/table-races.test.ts +++ b/ui/frontend/tests/table-races.test.ts @@ -226,6 +226,18 @@ describe("races table", () => { expect(names).toEqual(["Beta", "Gamma", "Alpha"]); }); + test("clicking the already-active stance is a no-op (no command queued)", async () => { + const ui = mountTable( + makeReport([race({ name: "Andori", relation: "WAR" })]), + ); + await fireEvent.click(ui.getByTestId("races-stance-war")); + // Give the async handler one microtask to settle, then assert + // the draft remained empty — the click matched the current + // stance, so nothing should land in the order queue. + await Promise.resolve(); + expect(draft.commands).toHaveLength(0); + }); + test("clicking PEACE on a WAR row appends setDiplomaticStance and flips the overlay", async () => { const ui = mountTable( makeReport([race({ name: "Andori", relation: "WAR" })]), diff --git a/ui/frontend/vitest.config.ts b/ui/frontend/vitest.config.ts index f53490d..63037a5 100644 --- a/ui/frontend/vitest.config.ts +++ b/ui/frontend/vitest.config.ts @@ -1,9 +1,15 @@ import { defineConfig, mergeConfig } from "vitest/config"; import viteConfig from "./vite.config"; -export default mergeConfig( - viteConfig, - defineConfig({ +// `vite.config.ts` exports a `defineConfig(({ mode }) => …)` callback +// so the dev server can load `.env*` files through Vite's `loadEnv`. +// `mergeConfig` does not accept callback-form configs, so resolve the +// callback here with the same context Vitest would supply, then hand +// the plain object to the merge. +export default defineConfig(async (ctx) => { + const resolved = + typeof viteConfig === "function" ? await viteConfig(ctx) : viteConfig; + return mergeConfig(resolved, { resolve: { // Force the browser entry of Svelte so `mount` is available in jsdom. conditions: ["browser"], @@ -14,5 +20,5 @@ export default mergeConfig( globals: true, setupFiles: ["./tests/setup.ts"], }, - }), -); + }); +});