From e2a4790f6cd9c66b19cf2abc25c59f971612cbe6 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Mon, 11 May 2026 11:19:57 +0200 Subject: [PATCH] ui/phase-22: skip the no-op stance click in the races table Clicking the already-active WAR/PEACE button still appended a \`setDiplomaticStance\` whose \`relation\` matched the row's current value. The engine would accept the duplicate harmlessly, but the order tab inflates with rows that say nothing and every auto-sync re-ships the redundant payload. Compare against the overlayed stance (so a queued-but-not-applied change suppresses a re-click that matches the *intended* state, not just the server snapshot) and short-circuit when they agree. Mirrors the vote picker, which already had the same guard. vitest.config.ts: \`mergeConfig\` refuses callback-form base configs, so resolve \`vite.config.ts\`'s callback with the test context first and merge the plain object. Surfaced after the \`loadEnv\` migration switched the root config to the callback form. Co-Authored-By: Claude Opus 4.7 --- .../src/lib/active-view/table-races.svelte | 9 +++++++++ ui/frontend/tests/table-races.test.ts | 12 ++++++++++++ ui/frontend/vitest.config.ts | 16 +++++++++++----- 3 files changed, 32 insertions(+), 5 deletions(-) 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"], }, - }), -); + }); +});