Skip to content

Commit 77158fe

Browse files
committed
refactor: extract splitAndSortEvents and add unit tests
1 parent bbf30ab commit 77158fe

2 files changed

Lines changed: 176 additions & 28 deletions

File tree

src/client/graphics/layers/EventsDisplay.ts

Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import donateGoldIcon from "/images/DonateGoldIconWhite.svg?url";
4141
import nukeIcon from "/images/NukeIconWhite.svg?url";
4242
import swordIcon from "/images/SwordIconWhite.svg?url";
4343

44-
interface GameEvent {
44+
export interface GameEvent {
4545
description: string;
4646
unsafeDescription?: boolean;
4747
buttons?: {
@@ -63,6 +63,43 @@ interface GameEvent {
6363
allianceID?: number;
6464
}
6565

66+
/**
67+
* Splits events into pinned (actionable alliance requests/renewals) and
68+
* regular events. Regular events are sorted by priority then createdAt.
69+
*/
70+
export function splitAndSortEvents(events: GameEvent[]): {
71+
pinnedEvents: GameEvent[];
72+
regularEvents: GameEvent[];
73+
} {
74+
const pinnedEvents = events.filter(
75+
(event) =>
76+
(event.type === MessageType.ALLIANCE_REQUEST ||
77+
event.type === MessageType.RENEW_ALLIANCE) &&
78+
event.buttons &&
79+
event.buttons.length > 0,
80+
);
81+
const regularEvents = events.filter(
82+
(event) =>
83+
!(
84+
(event.type === MessageType.ALLIANCE_REQUEST ||
85+
event.type === MessageType.RENEW_ALLIANCE) &&
86+
event.buttons &&
87+
event.buttons.length > 0
88+
),
89+
);
90+
91+
regularEvents.sort((a, b) => {
92+
const aPrior = a.priority ?? 100000;
93+
const bPrior = b.priority ?? 100000;
94+
if (aPrior === bPrior) {
95+
return a.createdAt - b.createdAt;
96+
}
97+
return bPrior - aPrior;
98+
});
99+
100+
return { pinnedEvents, regularEvents };
101+
}
102+
66103
@customElement("events-display")
67104
export class EventsDisplay extends LitElement implements Layer {
68105
public eventBus: EventBus;
@@ -758,33 +795,8 @@ export class EventsDisplay extends LitElement implements Layer {
758795
return !this.eventsFilters.get(category);
759796
});
760797

761-
// Separate actionable alliance events (requests & renewals) so they
762-
// can be rendered in a pinned, non-scrolling section above the feed.
763-
const pinnedEvents = filteredEvents.filter(
764-
(event) =>
765-
(event.type === MessageType.ALLIANCE_REQUEST ||
766-
event.type === MessageType.RENEW_ALLIANCE) &&
767-
event.buttons &&
768-
event.buttons.length > 0,
769-
);
770-
const regularEvents = filteredEvents.filter(
771-
(event) =>
772-
!(
773-
(event.type === MessageType.ALLIANCE_REQUEST ||
774-
event.type === MessageType.RENEW_ALLIANCE) &&
775-
event.buttons &&
776-
event.buttons.length > 0
777-
),
778-
);
779-
780-
regularEvents.sort((a, b) => {
781-
const aPrior = a.priority ?? 100000;
782-
const bPrior = b.priority ?? 100000;
783-
if (aPrior === bPrior) {
784-
return a.createdAt - b.createdAt;
785-
}
786-
return bPrior - aPrior;
787-
});
798+
const { pinnedEvents, regularEvents } =
799+
splitAndSortEvents(filteredEvents);
788800

789801
return html`
790802
${styles}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { describe, expect, it } from "vitest";
2+
import { MessageType } from "../../../../src/core/game/Game";
3+
import {
4+
GameEvent,
5+
splitAndSortEvents,
6+
} from "../../../../src/client/graphics/layers/EventsDisplay";
7+
8+
const dummyButtons = [
9+
{ text: "Accept", className: "btn", action: () => {} },
10+
{ text: "Reject", className: "btn-info", action: () => {} },
11+
];
12+
13+
function makeEvent(overrides: Partial<GameEvent> = {}): GameEvent {
14+
return {
15+
description: "test event",
16+
type: MessageType.ATTACK_FAILED,
17+
createdAt: 100,
18+
...overrides,
19+
} as GameEvent;
20+
}
21+
22+
describe("splitAndSortEvents", () => {
23+
it("should pin ALLIANCE_REQUEST events that have buttons", () => {
24+
const events = [
25+
makeEvent({
26+
type: MessageType.ALLIANCE_REQUEST,
27+
buttons: dummyButtons,
28+
}),
29+
makeEvent({ type: MessageType.ATTACK_FAILED }),
30+
];
31+
32+
const { pinnedEvents, regularEvents } = splitAndSortEvents(events);
33+
34+
expect(pinnedEvents).toHaveLength(1);
35+
expect(pinnedEvents[0].type).toBe(MessageType.ALLIANCE_REQUEST);
36+
expect(regularEvents).toHaveLength(1);
37+
expect(regularEvents[0].type).toBe(MessageType.ATTACK_FAILED);
38+
});
39+
40+
it("should pin RENEW_ALLIANCE events that have buttons", () => {
41+
const events = [
42+
makeEvent({
43+
type: MessageType.RENEW_ALLIANCE,
44+
buttons: dummyButtons,
45+
}),
46+
makeEvent({ type: MessageType.CHAT }),
47+
];
48+
49+
const { pinnedEvents, regularEvents } = splitAndSortEvents(events);
50+
51+
expect(pinnedEvents).toHaveLength(1);
52+
expect(pinnedEvents[0].type).toBe(MessageType.RENEW_ALLIANCE);
53+
expect(regularEvents).toHaveLength(1);
54+
expect(regularEvents[0].type).toBe(MessageType.CHAT);
55+
});
56+
57+
it("should NOT pin alliance events without buttons", () => {
58+
const events = [
59+
makeEvent({ type: MessageType.ALLIANCE_REQUEST }),
60+
makeEvent({ type: MessageType.RENEW_ALLIANCE, buttons: [] }),
61+
makeEvent({ type: MessageType.ALLIANCE_ACCEPTED }),
62+
];
63+
64+
const { pinnedEvents, regularEvents } = splitAndSortEvents(events);
65+
66+
expect(pinnedEvents).toHaveLength(0);
67+
expect(regularEvents).toHaveLength(3);
68+
});
69+
70+
it("should NOT pin non-alliance events even if they have buttons", () => {
71+
const events = [
72+
makeEvent({
73+
type: MessageType.ALLIANCE_BROKEN,
74+
buttons: dummyButtons,
75+
}),
76+
];
77+
78+
const { pinnedEvents, regularEvents } = splitAndSortEvents(events);
79+
80+
expect(pinnedEvents).toHaveLength(0);
81+
expect(regularEvents).toHaveLength(1);
82+
});
83+
84+
it("should sort regular events by priority (descending) then createdAt", () => {
85+
const events = [
86+
makeEvent({ description: "low-prio-early", priority: 0, createdAt: 1 }),
87+
makeEvent({ description: "high-prio", priority: 100, createdAt: 5 }),
88+
makeEvent({ description: "low-prio-late", priority: 0, createdAt: 10 }),
89+
makeEvent({ description: "default-prio", createdAt: 3 }),
90+
];
91+
92+
const { regularEvents } = splitAndSortEvents(events);
93+
94+
// default priority (100000) > 100 > 0 – descending
95+
expect(regularEvents.map((e) => e.description)).toEqual([
96+
"default-prio",
97+
"high-prio",
98+
"low-prio-early",
99+
"low-prio-late",
100+
]);
101+
});
102+
103+
it("should handle an empty events list", () => {
104+
const { pinnedEvents, regularEvents } = splitAndSortEvents([]);
105+
106+
expect(pinnedEvents).toHaveLength(0);
107+
expect(regularEvents).toHaveLength(0);
108+
});
109+
110+
it("should pin multiple alliance events simultaneously", () => {
111+
const events = [
112+
makeEvent({
113+
type: MessageType.ALLIANCE_REQUEST,
114+
buttons: dummyButtons,
115+
description: "request 1",
116+
}),
117+
makeEvent({
118+
type: MessageType.RENEW_ALLIANCE,
119+
buttons: dummyButtons,
120+
description: "renew 1",
121+
}),
122+
makeEvent({
123+
type: MessageType.ALLIANCE_REQUEST,
124+
buttons: dummyButtons,
125+
description: "request 2",
126+
}),
127+
makeEvent({ type: MessageType.CHAT, description: "chat msg" }),
128+
];
129+
130+
const { pinnedEvents, regularEvents } = splitAndSortEvents(events);
131+
132+
expect(pinnedEvents).toHaveLength(3);
133+
expect(regularEvents).toHaveLength(1);
134+
expect(regularEvents[0].description).toBe("chat msg");
135+
});
136+
});

0 commit comments

Comments
 (0)