Files
OpenFrontIO/tests/client/graphics/layers/ActionableEventsAlliance.test.ts
T
Evan 41ef675e98 Improve Notification Panel (#3913)
Resolves #3910

## Description:

- Split the events HUD into two components: a new
**`<actionable-events>`** that owns alliance prompts (request / renew)
and a slimmed-down **`<events-display>`** for everything else.
- Reworked `<events-display>` into two visual tiers: dim/scrolling tier
2 on top (trade results, unit losses, donations, alliance status),
prominent tier 1 anchored at the bottom (inbound nukes, naval invasion,
attack requests, alliance broken, conquered player, chat). Tier 2 caps
at the 4 newest entries; events expire after 8s.
- Added a transient **+gold pip** above the gold pill in
`<control-panel>`, animated with a small fade-in. Fires for trade ships,
trains, donations, and conquest. Trade-ship and train arrivals are
removed from the events scroll since they're surfaced here instead.
- New `MessageType.NUKE_DETONATED` and a server-side emission in
`NukeExecution.detonate` — once an inbound nuke lands or gets
intercepted, the inbound warning vanishes and a "detonated" entry takes
its place.
- `displayMessage` gained optional `unitID` and `focusPlayerID` params
so events can link to a unit or a player. Unit captures and destructions
now navigate to the unit's last tile when clicked; donations navigate to
the other player.
- ActionableEvents card width matches `<events-display>`; cards persist
until the user clicks Accept/Reject/Renew/Ignore or the server-side
request timeout expires.
- Removed the in-events category filter UI and the gold-amount banner —
`<events-display>` is now a lightweight log that hides entirely when
empty.

<img width="570" height="444" alt="Screenshot 2026-05-21 at 1 42 30 PM"
src="https://github.com/user-attachments/assets/f103efb3-0e11-4b72-a11b-91ff6896177c"
/>

<img width="430" height="296" alt="Screenshot 2026-05-21 at 1 41 34 PM"
src="https://github.com/user-attachments/assets/ae58475a-b252-4aa6-9ce5-99dea7575ce3"
/>

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

evan
2026-05-21 19:50:10 +01:00

212 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { GameUpdateType } from "../../../../src/core/game/GameUpdates";
vi.mock("lit", () => ({
html: () => {},
LitElement: class {},
}));
vi.mock("lit/decorators.js", () => ({
customElement: () => (clazz: any) => clazz,
query: () => () => {},
state: () => () => {},
property: () => () => {},
}));
vi.mock("lit/directive.js", () => ({
DirectiveResult: class {},
}));
vi.mock("lit/directives/unsafe-html.js", () => ({
unsafeHTML: () => {},
UnsafeHTMLDirective: class {},
}));
import { ActionableEvents } from "../../../../src/client/hud/layers/ActionableEvents";
import { MessageType } from "../../../../src/core/game/Game";
describe("ActionableEvents - alliance renewal cleanup (allianceID based)", () => {
function makeRenewal(
allianceID: number,
focusID: number,
description = "Alliance about to expire",
) {
return {
description,
type: MessageType.RENEW_ALLIANCE,
allianceID,
focusID,
createdAt: 0,
};
}
test("removes ONLY renewal events for the broken alliance", () => {
const display = new ActionableEvents();
const allianceAB = 1;
const allianceAC = 2;
const allianceBC = 3;
(display as any).events = [
makeRenewal(allianceAB, 1), // AB
makeRenewal(allianceAC, 1), // AC
makeRenewal(allianceBC, 2), // BC
];
// Break alliance AB
(display as any).removeAllianceRenewalEvents(allianceAB);
const remaining = (display as any).events;
// AB renewal removed
expect(remaining.some((e: any) => e.allianceID === allianceAB)).toBe(false);
// Other alliances untouched
expect(remaining.some((e: any) => e.allianceID === allianceAC)).toBe(true);
expect(remaining.some((e: any) => e.allianceID === allianceBC)).toBe(true);
});
test("does NOT remove renewals just because the same player is involved", () => {
const display = new ActionableEvents();
const allianceAB = 10;
const allianceAC = 11;
(display as any).events = [
makeRenewal(allianceAB, 1), // Player 1 involved
makeRenewal(allianceAC, 1), // Same player, different alliance
];
(display as any).removeAllianceRenewalEvents(allianceAB);
const remaining = (display as any).events;
expect(remaining.length).toBe(1);
expect(remaining[0].allianceID).toBe(allianceAC);
});
test("breaking one alliance does not affect renewals between other players", () => {
const display = new ActionableEvents();
const allianceAB = 100;
const allianceCD = 200;
(display as any).events = [
makeRenewal(allianceAB, 1), // AB
makeRenewal(allianceCD, 3), // CD
];
(display as any).removeAllianceRenewalEvents(allianceAB);
const remaining = (display as any).events;
expect(remaining.length).toBe(1);
expect(remaining[0].allianceID).toBe(allianceCD);
});
test("onAllianceExtensionEvent removes renewal when playerID matches myPlayer", () => {
const display = new ActionableEvents();
const allianceID = 42;
const mySmallID = 7;
(display as any).game = {
myPlayer: () => ({ smallID: () => mySmallID }),
};
(display as any).requestUpdate = () => {};
(display as any).events = [makeRenewal(allianceID, mySmallID)];
(display as any).onAllianceExtensionEvent({
type: GameUpdateType.AllianceExtension,
playerID: mySmallID,
allianceID,
});
const remaining = (display as any).events;
expect(remaining.some((e: any) => e.allianceID === allianceID)).toBe(false);
});
test("onAllianceExtensionEvent keeps renewal when playerID does not match myPlayer", () => {
const display = new ActionableEvents();
const allianceID = 42;
const mySmallID = 7;
const otherSmallID = 9;
(display as any).game = {
myPlayer: () => ({ smallID: () => mySmallID }),
};
(display as any).requestUpdate = () => {};
(display as any).events = [makeRenewal(allianceID, mySmallID)];
(display as any).onAllianceExtensionEvent({
type: "AllianceExtension",
playerID: otherSmallID,
allianceID,
});
const remaining = (display as any).events;
expect(remaining.some((e: any) => e.allianceID === allianceID)).toBe(true);
});
test("onAllianceExtensionEvent keeps renewal when myPlayer is null", () => {
const display = new ActionableEvents();
const allianceID = 42;
(display as any).game = {
myPlayer: () => null,
};
(display as any).requestUpdate = () => {};
(display as any).events = [makeRenewal(allianceID, 1)];
(display as any).onAllianceExtensionEvent({
type: "AllianceExtension",
playerID: 1,
allianceID,
});
const remaining = (display as any).events;
expect(remaining.some((e: any) => e.allianceID === allianceID)).toBe(true);
});
test("does not affect non-RENEW_ALLIANCE events", () => {
const display = new ActionableEvents();
(display as any).events = [
{
description: "Alliance broken",
type: MessageType.ALLIANCE_BROKEN,
createdAt: 0,
},
{
description: "Alliance accepted",
type: MessageType.ALLIANCE_ACCEPTED,
createdAt: 0,
},
{
description: "Renewal",
type: MessageType.RENEW_ALLIANCE,
allianceID: 999,
createdAt: 0,
},
];
(display as any).removeAllianceRenewalEvents(999);
const remaining = (display as any).events;
expect(
remaining.some((e: any) => e.type === MessageType.ALLIANCE_BROKEN),
).toBe(true);
expect(
remaining.some((e: any) => e.type === MessageType.ALLIANCE_ACCEPTED),
).toBe(true);
expect(
remaining.some((e: any) => e.type === MessageType.RENEW_ALLIANCE),
).toBe(false);
});
});