ui/phase-20: pick-first Send + lock after Modernize/Dismantle/Transfer

Send no longer carries a destination control inside the form: a
click on the action drops the inspector straight into map-pick
mode, and the form (ship count + confirm) only mounts after the
player chooses a destination. Cancelling the picker leaves no
form behind.

A queued Modernize / Dismantle / Transfer for a given group
locks every action button on its inspector and surfaces a banner
that points the player at the order list. Cancelling the queued
entry from the order tab releases the lock on the next render —
the derivation watches draft.commands directly. Send / Load /
Unload / Split / Join Fleet do not lock; Send is naturally
followed by an out-of-orbit state at turn cutoff, the rest can
stack legitimately.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-10 17:20:48 +02:00
parent de824dfc9a
commit ac14eaff10
7 changed files with 332 additions and 77 deletions
@@ -262,3 +262,108 @@ describe("ship-group inspector — implicit split + action", () => {
expect(cmd.groupId).toBe("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
});
});
describe("ship-group inspector — destructive command lock", () => {
const ALL_ACTION_TESTIDS = [
"inspector-ship-group-action-split",
"inspector-ship-group-action-send",
"inspector-ship-group-action-load",
"inspector-ship-group-action-unload",
"inspector-ship-group-action-modernize",
"inspector-ship-group-action-dismantle",
"inspector-ship-group-action-transfer",
"inspector-ship-group-action-join-fleet",
];
test("a queued dismantleShipGroup disables every action with the lock tooltip", async () => {
const groupId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
await draft.add({
kind: "dismantleShipGroup",
id: crypto.randomUUID(),
groupId,
});
const ui = mount(localGroup({ id: groupId, count: 3, cargo: "MAT", load: 0.5 }));
const banner = ui.getByTestId("inspector-ship-group-actions-locked");
expect(banner).toHaveTextContent(/dismantle/i);
for (const id of ALL_ACTION_TESTIDS) {
const button = ui.getByTestId(id);
expect(button).toBeDisabled();
expect(button.getAttribute("title")).toMatch(/order is already queued/i);
}
});
test("a queued upgradeShipGroup locks the inspector and reports modernize as the kind", async () => {
const groupId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
await draft.add({
kind: "upgradeShipGroup",
id: crypto.randomUUID(),
groupId,
tech: "ALL",
level: 0,
});
const ui = mount(localGroup({ id: groupId, count: 2 }));
expect(ui.getByTestId("inspector-ship-group-actions-locked")).toHaveTextContent(
/modernize/i,
);
});
test("a queued transferShipGroup locks the inspector and reports transfer as the kind", async () => {
const groupId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
await draft.add({
kind: "transferShipGroup",
id: crypto.randomUUID(),
groupId,
acceptor: "Aliens",
});
const ui = mount(localGroup({ id: groupId }));
expect(ui.getByTestId("inspector-ship-group-actions-locked")).toHaveTextContent(
/transfer/i,
);
});
test("a queued sendShipGroup does NOT lock the group", async () => {
const groupId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
await draft.add({
kind: "sendShipGroup",
id: crypto.randomUUID(),
groupId,
destinationPlanetNumber: 99,
});
const ui = mount(localGroup({ id: groupId, count: 3 }));
expect(
ui.queryByTestId("inspector-ship-group-actions-locked"),
).toBeNull();
expect(ui.getByTestId("inspector-ship-group-action-split")).not.toBeDisabled();
});
test("a destructive command targeting a different group does not lock this one", async () => {
await draft.add({
kind: "dismantleShipGroup",
id: crypto.randomUUID(),
groupId: "ffffffff-ffff-ffff-ffff-ffffffffffff",
});
const ui = mount(localGroup({ count: 3 }));
expect(
ui.queryByTestId("inspector-ship-group-actions-locked"),
).toBeNull();
});
test("removing the destructive command from the draft releases the lock", async () => {
const groupId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
const cmdId = crypto.randomUUID();
await draft.add({
kind: "dismantleShipGroup",
id: cmdId,
groupId,
});
const ui = mount(localGroup({ id: groupId, count: 3 }));
expect(ui.getByTestId("inspector-ship-group-actions-locked")).toBeInTheDocument();
await draft.remove(cmdId);
await waitFor(() => {
expect(
ui.queryByTestId("inspector-ship-group-actions-locked"),
).toBeNull();
});
expect(ui.getByTestId("inspector-ship-group-action-split")).not.toBeDisabled();
});
});