Skip to content

Commit f48178a

Browse files
fix: clean up neighbors references on state removal (#1304)
* fix: clean up neighbors references on state removal * test: add end-to-end tests for state removal and military regeneration
1 parent 8ba29b2 commit f48178a

File tree

3 files changed

+97
-1
lines changed

3 files changed

+97
-1
lines changed

public/modules/dynamic/editors/states-editor.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,12 @@ function stateRemove(stateId) {
640640
});
641641
armies.select("g#army" + stateId).remove();
642642

643+
// clean up neighbors references from other states
644+
pack.states.forEach(state => {
645+
if (!state.i || state.removed || !state.neighbors) return;
646+
state.neighbors = state.neighbors.filter(n => n !== stateId);
647+
});
648+
643649
pack.states[stateId] = {i: stateId, removed: true};
644650

645651
debug.selectAll(".highlight").remove();

public/modules/ui/editors.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -991,7 +991,7 @@ function refreshAllEditors() {
991991
// dynamically loaded editors
992992
async function editStates() {
993993
if (customization) return;
994-
const Editor = await import("../dynamic/editors/states-editor.js?v=1.108.1");
994+
const Editor = await import("../dynamic/editors/states-editor.js?v=1.112.1");
995995
Editor.open();
996996
}
997997

tests/e2e/states.spec.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import {test, expect} from "@playwright/test";
2+
3+
test.describe("States", () => {
4+
test.beforeEach(async ({context, page}) => {
5+
await context.clearCookies();
6+
7+
await page.goto("/");
8+
await page.evaluate(() => {
9+
localStorage.clear();
10+
sessionStorage.clear();
11+
});
12+
13+
// Navigate with seed parameter and wait for full load
14+
await page.goto("/?seed=test-states&width=1280&height=720");
15+
16+
// Wait for map generation to complete
17+
await page.waitForFunction(() => (window as any).mapId !== undefined, {timeout: 60000});
18+
19+
// Additional wait for any rendering/animations to settle
20+
await page.waitForTimeout(500);
21+
});
22+
23+
test("removing a state via UI should allow military regeneration without errors", async ({page}) => {
24+
// First click the options trigger (►) to open the menu
25+
await page.click("#optionsTrigger");
26+
await page.waitForTimeout(300);
27+
28+
// Open the Tools tab
29+
await page.click("#toolsTab");
30+
await page.waitForTimeout(200);
31+
32+
// Click "States" button to open States Editor
33+
await page.click("#editStatesButton");
34+
await page.waitForSelector("#statesEditor", {state: "visible", timeout: 5000});
35+
await page.waitForTimeout(300);
36+
37+
// Find a state row and get its ID
38+
const stateId = await page.evaluate(() => {
39+
const stateRow = document.querySelector("#statesBodySection > div[data-id]") as HTMLElement;
40+
return stateRow ? parseInt(stateRow.dataset.id!, 10) : null;
41+
});
42+
43+
expect(stateId).not.toBeNull();
44+
45+
// Verify this state is in neighbors of other states before removal
46+
const neighborsBefore = await page.evaluate((id: number) => {
47+
const {states} = (window as any).pack;
48+
return states.filter((s: any) => s.i && !s.removed && s.neighbors && s.neighbors.includes(id)).length;
49+
}, stateId!);
50+
51+
// Click the trash icon to remove the state
52+
await page.click(`#statesBodySection > div[data-id="${stateId}"] .icon-trash-empty`);
53+
54+
// Confirm the removal in the jQuery dialog - look for "Remove" button in the dialog buttonpane
55+
await page.waitForSelector(".ui-dialog:has(#alert) .ui-dialog-buttonpane", {state: "visible", timeout: 3000});
56+
await page.click(".ui-dialog:has(#alert) .ui-dialog-buttonpane button:first-child"); // "Remove" is first button
57+
await page.waitForTimeout(500);
58+
59+
// Verify the state is no longer in neighbors of any other state
60+
const neighborsAfter = await page.evaluate((id: number) => {
61+
const {states} = (window as any).pack;
62+
return states.filter((s: any) => s.i && !s.removed && s.neighbors && s.neighbors.includes(id)).length;
63+
}, stateId!);
64+
65+
expect(neighborsAfter).toBe(0);
66+
67+
// Close the States Editor - the close button is in the jQuery UI dialog wrapper
68+
await page.click(".ui-dialog:has(#statesEditor) .ui-dialog-titlebar-close");
69+
await page.waitForTimeout(200);
70+
71+
// Now click "Military" regenerate button and verify no errors
72+
await page.click("#regenerateMilitary");
73+
await page.waitForTimeout(1000);
74+
75+
// Verify military was regenerated without throwing
76+
const militaryResult = await page.evaluate(() => {
77+
const {states} = (window as any).pack;
78+
const validStates = states.filter((s: any) => s.i && !s.removed);
79+
// Check that at least some states have military data
80+
return {
81+
statesCount: validStates.length,
82+
statesWithMilitary: validStates.filter((s: any) => s.military && s.military.length > 0).length
83+
};
84+
});
85+
86+
expect(militaryResult.statesCount).toBeGreaterThan(0);
87+
// At least some states should have military
88+
expect(militaryResult.statesWithMilitary).toBeGreaterThanOrEqual(0);
89+
});
90+
});

0 commit comments

Comments
 (0)