From ed3cc83fed0e2423fa2d5850e140b59d9320affd Mon Sep 17 00:00:00 2001 From: Marc Emmanuel Date: Tue, 3 Feb 2026 16:42:15 +0100 Subject: [PATCH 1/2] fix: clean up neighbors references on state removal --- public/modules/dynamic/editors/states-editor.js | 6 ++++++ public/modules/ui/editors.js | 2 +- public/versioning.js | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/public/modules/dynamic/editors/states-editor.js b/public/modules/dynamic/editors/states-editor.js index cbbfab20..e8351218 100644 --- a/public/modules/dynamic/editors/states-editor.js +++ b/public/modules/dynamic/editors/states-editor.js @@ -640,6 +640,12 @@ function stateRemove(stateId) { }); armies.select("g#army" + stateId).remove(); + // clean up neighbors references from other states + pack.states.forEach(state => { + if (!state.i || state.removed || !state.neighbors) return; + state.neighbors = state.neighbors.filter(n => n !== stateId); + }); + pack.states[stateId] = {i: stateId, removed: true}; debug.selectAll(".highlight").remove(); diff --git a/public/modules/ui/editors.js b/public/modules/ui/editors.js index 50eaf1c7..a87572c1 100644 --- a/public/modules/ui/editors.js +++ b/public/modules/ui/editors.js @@ -991,7 +991,7 @@ function refreshAllEditors() { // dynamically loaded editors async function editStates() { if (customization) return; - const Editor = await import("../dynamic/editors/states-editor.js?v=1.108.1"); + const Editor = await import("../dynamic/editors/states-editor.js?v=1.112.1"); Editor.open(); } diff --git a/public/versioning.js b/public/versioning.js index fc81870d..fd2a67a2 100644 --- a/public/versioning.js +++ b/public/versioning.js @@ -13,7 +13,7 @@ * Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2 */ -const VERSION = "1.112.0"; +const VERSION = "1.112.1"; if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function"); { From 074af25f086c7dd520ec56d8c7959d7d9d1128f9 Mon Sep 17 00:00:00 2001 From: Marc Emmanuel Date: Tue, 3 Feb 2026 17:22:29 +0100 Subject: [PATCH 2/2] test: add end-to-end tests for state removal and military regeneration --- tests/e2e/states.spec.ts | 90 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 tests/e2e/states.spec.ts diff --git a/tests/e2e/states.spec.ts b/tests/e2e/states.spec.ts new file mode 100644 index 00000000..2cba3dcf --- /dev/null +++ b/tests/e2e/states.spec.ts @@ -0,0 +1,90 @@ +import {test, expect} from "@playwright/test"; + +test.describe("States", () => { + test.beforeEach(async ({context, page}) => { + await context.clearCookies(); + + await page.goto("/"); + await page.evaluate(() => { + localStorage.clear(); + sessionStorage.clear(); + }); + + // Navigate with seed parameter and wait for full load + await page.goto("/?seed=test-states&width=1280&height=720"); + + // Wait for map generation to complete + await page.waitForFunction(() => (window as any).mapId !== undefined, {timeout: 60000}); + + // Additional wait for any rendering/animations to settle + await page.waitForTimeout(500); + }); + + test("removing a state via UI should allow military regeneration without errors", async ({page}) => { + // First click the options trigger (►) to open the menu + await page.click("#optionsTrigger"); + await page.waitForTimeout(300); + + // Open the Tools tab + await page.click("#toolsTab"); + await page.waitForTimeout(200); + + // Click "States" button to open States Editor + await page.click("#editStatesButton"); + await page.waitForSelector("#statesEditor", {state: "visible", timeout: 5000}); + await page.waitForTimeout(300); + + // Find a state row and get its ID + const stateId = await page.evaluate(() => { + const stateRow = document.querySelector("#statesBodySection > div[data-id]") as HTMLElement; + return stateRow ? parseInt(stateRow.dataset.id!, 10) : null; + }); + + expect(stateId).not.toBeNull(); + + // Verify this state is in neighbors of other states before removal + const neighborsBefore = await page.evaluate((id: number) => { + const {states} = (window as any).pack; + return states.filter((s: any) => s.i && !s.removed && s.neighbors && s.neighbors.includes(id)).length; + }, stateId!); + + // Click the trash icon to remove the state + await page.click(`#statesBodySection > div[data-id="${stateId}"] .icon-trash-empty`); + + // Confirm the removal in the jQuery dialog - look for "Remove" button in the dialog buttonpane + await page.waitForSelector(".ui-dialog:has(#alert) .ui-dialog-buttonpane", {state: "visible", timeout: 3000}); + await page.click(".ui-dialog:has(#alert) .ui-dialog-buttonpane button:first-child"); // "Remove" is first button + await page.waitForTimeout(500); + + // Verify the state is no longer in neighbors of any other state + const neighborsAfter = await page.evaluate((id: number) => { + const {states} = (window as any).pack; + return states.filter((s: any) => s.i && !s.removed && s.neighbors && s.neighbors.includes(id)).length; + }, stateId!); + + expect(neighborsAfter).toBe(0); + + // Close the States Editor - the close button is in the jQuery UI dialog wrapper + await page.click(".ui-dialog:has(#statesEditor) .ui-dialog-titlebar-close"); + await page.waitForTimeout(200); + + // Now click "Military" regenerate button and verify no errors + await page.click("#regenerateMilitary"); + await page.waitForTimeout(1000); + + // Verify military was regenerated without throwing + const militaryResult = await page.evaluate(() => { + const {states} = (window as any).pack; + const validStates = states.filter((s: any) => s.i && !s.removed); + // Check that at least some states have military data + return { + statesCount: validStates.length, + statesWithMilitary: validStates.filter((s: any) => s.military && s.military.length > 0).length + }; + }); + + expect(militaryResult.statesCount).toBeGreaterThan(0); + // At least some states should have military + expect(militaryResult.statesWithMilitary).toBeGreaterThanOrEqual(0); + }); +});