Skip to content

Commit 5563d0c

Browse files
committed
test: add 63 new tests across utilities, composables, and stores
Add 9 new spec files and extend useSound.spec.ts to improve test coverage from 120 to 183 tests. Covers pure utility functions (cleanMapName, separateByCapitalLetters, uuid), composables with state logic (useRightSidebar, useInvites), auth/login utilities (loginLinks, setupOptions), and Pinia stores (AuthStore role hierarchy, SearchStore player search).
1 parent 9b587ba commit 5563d0c

11 files changed

Lines changed: 750 additions & 7 deletions

composables/useInvites.spec.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
const matchmakingMock = {
2+
matchInvites: [] as any[],
3+
lobbyInvites: [] as any[],
4+
friends: [] as any[],
5+
};
6+
7+
const authMock = {
8+
me: { steam_id: "my-steam-id" } as any,
9+
};
10+
11+
vi.mock("~/stores/MatchmakingStore", () => ({
12+
useMatchmakingStore: () => matchmakingMock,
13+
}));
14+
15+
// useMatchmakingStore is auto-imported in Nuxt, provide global too
16+
(globalThis as any).useMatchmakingStore = () => matchmakingMock;
17+
(globalThis as any).useAuthStore = () => authMock;
18+
19+
import { useInvites } from "./useInvites";
20+
21+
describe("useInvites", () => {
22+
beforeEach(() => {
23+
matchmakingMock.matchInvites = [];
24+
matchmakingMock.lobbyInvites = [];
25+
matchmakingMock.friends = [];
26+
authMock.me = { steam_id: "my-steam-id" };
27+
});
28+
29+
it("returns pendingFriends sorted by name", () => {
30+
matchmakingMock.friends = [
31+
{ name: "Charlie", status: "Pending", invited_by_steam_id: "other" },
32+
{ name: "Alice", status: "Pending", invited_by_steam_id: "other" },
33+
{ name: "Bob", status: "Pending", invited_by_steam_id: "other" },
34+
];
35+
36+
const { pendingFriends } = useInvites();
37+
expect(pendingFriends.value.map((f: any) => f.name)).toEqual(["Alice", "Bob", "Charlie"]);
38+
});
39+
40+
it("filters pendingFriends: status Pending AND not invited by current user", () => {
41+
matchmakingMock.friends = [
42+
{ name: "Pending-Other", status: "Pending", invited_by_steam_id: "other-id" },
43+
{ name: "Pending-Me", status: "Pending", invited_by_steam_id: "my-steam-id" },
44+
{ name: "Accepted", status: "Accepted", invited_by_steam_id: "other-id" },
45+
];
46+
47+
const { pendingFriends } = useInvites();
48+
expect(pendingFriends.value).toHaveLength(1);
49+
expect(pendingFriends.value[0].name).toBe("Pending-Other");
50+
});
51+
52+
it("hasInvites true when matchInvites non-empty", () => {
53+
matchmakingMock.matchInvites = [{ id: "1" }];
54+
const { hasInvites } = useInvites();
55+
expect(hasInvites.value).toBe(true);
56+
});
57+
58+
it("hasInvites true when lobbyInvites non-empty", () => {
59+
matchmakingMock.lobbyInvites = [{ id: "1" }];
60+
const { hasInvites } = useInvites();
61+
expect(hasInvites.value).toBe(true);
62+
});
63+
64+
it("hasInvites true when pendingFriends non-empty", () => {
65+
matchmakingMock.friends = [
66+
{ name: "Pending", status: "Pending", invited_by_steam_id: "other-id" },
67+
];
68+
const { hasInvites } = useInvites();
69+
expect(hasInvites.value).toBe(true);
70+
});
71+
72+
it("hasInvites false when all empty", () => {
73+
const { hasInvites } = useInvites();
74+
expect(hasInvites.value).toBe(false);
75+
});
76+
77+
it("totalCount sums all three arrays", () => {
78+
matchmakingMock.matchInvites = [{ id: "1" }, { id: "2" }];
79+
matchmakingMock.lobbyInvites = [{ id: "3" }];
80+
matchmakingMock.friends = [
81+
{ name: "Pending", status: "Pending", invited_by_steam_id: "other-id" },
82+
];
83+
84+
const { totalCount } = useInvites();
85+
expect(totalCount.value).toBe(4);
86+
});
87+
});
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { useRightSidebar } from "./useRightSidebar";
2+
3+
describe("useRightSidebar", () => {
4+
beforeEach(() => {
5+
const { setRightSidebarOpen, isPinned, togglePin } = useRightSidebar();
6+
setRightSidebarOpen(false);
7+
// Reset pin state if pinned
8+
if (isPinned.value) {
9+
togglePin();
10+
}
11+
localStorage.removeItem("right-hub-pinned");
12+
});
13+
14+
it("setRightSidebarOpen(true) opens sidebar", () => {
15+
const { setRightSidebarOpen, rightSidebarOpen } = useRightSidebar();
16+
setRightSidebarOpen(true);
17+
expect(rightSidebarOpen.value).toBe(true);
18+
});
19+
20+
it("toggleRightSidebar toggles open/closed", () => {
21+
const { toggleRightSidebar, rightSidebarOpen, setRightSidebarOpen } = useRightSidebar();
22+
setRightSidebarOpen(false);
23+
toggleRightSidebar();
24+
expect(rightSidebarOpen.value).toBe(true);
25+
toggleRightSidebar();
26+
expect(rightSidebarOpen.value).toBe(false);
27+
});
28+
29+
it("startHoverPeek opens sidebar when closed", () => {
30+
const { startHoverPeek, rightSidebarOpen } = useRightSidebar();
31+
expect(rightSidebarOpen.value).toBe(false);
32+
startHoverPeek();
33+
expect(rightSidebarOpen.value).toBe(true);
34+
});
35+
36+
it("startHoverPeek does nothing when already open", () => {
37+
const { startHoverPeek, setRightSidebarOpen, rightSidebarOpen } = useRightSidebar();
38+
setRightSidebarOpen(true);
39+
startHoverPeek();
40+
expect(rightSidebarOpen.value).toBe(true);
41+
});
42+
43+
it("endHoverPeek closes sidebar when not pinned", () => {
44+
const { startHoverPeek, endHoverPeek, rightSidebarOpen } = useRightSidebar();
45+
startHoverPeek();
46+
endHoverPeek();
47+
expect(rightSidebarOpen.value).toBe(false);
48+
});
49+
50+
it("endHoverPeek keeps sidebar open when pinned", () => {
51+
const { startHoverPeek, endHoverPeek, togglePin, rightSidebarOpen } = useRightSidebar();
52+
startHoverPeek();
53+
togglePin();
54+
endHoverPeek();
55+
expect(rightSidebarOpen.value).toBe(true);
56+
});
57+
58+
it("togglePin persists pin state to localStorage", () => {
59+
const { togglePin } = useRightSidebar();
60+
togglePin();
61+
expect(localStorage.getItem("right-hub-pinned")).toBe("1");
62+
});
63+
64+
it("togglePin removes localStorage key when unpinning", () => {
65+
const { togglePin } = useRightSidebar();
66+
togglePin(); // pin
67+
togglePin(); // unpin
68+
expect(localStorage.getItem("right-hub-pinned")).toBeNull();
69+
});
70+
});

composables/useSound.spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,36 @@ describe("useSound", () => {
3333
updateSettings(true, -1.0);
3434
expect(volume.value).toBe(0.0);
3535
});
36+
37+
it("defaults to enabled with volume 0.7", () => {
38+
const { isEnabled, volume } = useSound();
39+
expect(isEnabled.value).toBe(true);
40+
expect(volume.value).toBe(0.7);
41+
});
42+
43+
it("volume and isEnabled are readonly refs", () => {
44+
const { isEnabled, volume } = useSound();
45+
// readonly refs still have .value but writes are no-ops in production
46+
expect(typeof isEnabled.value).toBe("boolean");
47+
expect(typeof volume.value).toBe("number");
48+
});
49+
50+
it("playMatchFoundSound returns early when isEnabled is false", () => {
51+
const { updateSettings, playMatchFoundSound } = useSound();
52+
updateSettings(false);
53+
// Guard returns early before AudioContext — should not throw
54+
expect(() => playMatchFoundSound()).not.toThrow();
55+
});
56+
57+
it("playTickSound returns early when isEnabled is false", () => {
58+
const { updateSettings, playTickSound } = useSound();
59+
updateSettings(false);
60+
expect(() => playTickSound()).not.toThrow();
61+
});
62+
63+
it("playCountdownSound returns early when isEnabled is false", () => {
64+
const { updateSettings, playCountdownSound } = useSound();
65+
updateSettings(false);
66+
expect(() => playCountdownSound()).not.toThrow();
67+
});
3668
});

stores/AuthStore.spec.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { createPinia, setActivePinia } from "pinia";
2+
3+
vi.mock("~/stores/SearchStore", () => ({
4+
useSearchStore: vi.fn(),
5+
}));
6+
vi.mock("~/stores/MatchmakingStore", () => ({
7+
useMatchmakingStore: vi.fn(),
8+
}));
9+
vi.mock("~/stores/NotificationStore", () => ({
10+
useNotificationStore: vi.fn(),
11+
}));
12+
vi.mock("~/stores/ApplicationSettingsStore", () => ({
13+
useApplicationSettingsStore: vi.fn(),
14+
}));
15+
vi.mock("~/graphql/getGraphqlClient", () => ({
16+
default: vi.fn(),
17+
}));
18+
vi.mock("~/web-sockets/Socket", () => ({
19+
default: { connect: vi.fn() },
20+
}));
21+
vi.mock("~/graphql/graphqlGen", () => ({
22+
generateQuery: vi.fn(),
23+
generateSubscription: vi.fn(),
24+
}));
25+
vi.mock("~/graphql/meGraphql", () => ({
26+
meFields: {},
27+
}));
28+
29+
// Provide Nuxt auto-imports as globals
30+
(globalThis as any).useSearchStore = vi.fn();
31+
(globalThis as any).useMatchmakingStore = vi.fn();
32+
(globalThis as any).useNotificationStore = vi.fn();
33+
(globalThis as any).useApplicationSettingsStore = vi.fn();
34+
(globalThis as any).useMatchLobbyStore = vi.fn(() => ({
35+
subscribeToMyMatches: vi.fn(),
36+
subscribeToLiveMatches: vi.fn(),
37+
subscribeToLiveTournaments: vi.fn(),
38+
subscribeToOpenRegistrationTournaments: vi.fn(),
39+
subscribeToOpenMatches: vi.fn(),
40+
subscribeToChatTournaments: vi.fn(),
41+
subscribeToManagingMatches: vi.fn(),
42+
subscribeToManagingTournaments: vi.fn(),
43+
}));
44+
(globalThis as any).useNuxtApp = vi.fn(() => ({ $wsClient: { terminate: vi.fn(), on: vi.fn() } }));
45+
46+
import { useAuthStore } from "./AuthStore";
47+
import { e_player_roles_enum } from "~/generated/zeus";
48+
49+
describe("AuthStore", () => {
50+
beforeEach(() => {
51+
setActivePinia(createPinia());
52+
});
53+
54+
describe("isRoleAbove", () => {
55+
it("returns false when me is null", () => {
56+
const store = useAuthStore();
57+
expect(store.isRoleAbove(e_player_roles_enum.user)).toBe(false);
58+
});
59+
60+
it("returns true when user role >= target role", () => {
61+
const store = useAuthStore();
62+
store.me = { role: e_player_roles_enum.administrator } as any;
63+
expect(store.isRoleAbove(e_player_roles_enum.match_organizer)).toBe(true);
64+
});
65+
66+
it("returns false when user role < target role", () => {
67+
const store = useAuthStore();
68+
store.me = { role: e_player_roles_enum.user } as any;
69+
expect(store.isRoleAbove(e_player_roles_enum.administrator)).toBe(false);
70+
});
71+
72+
it("returns true for same role", () => {
73+
const store = useAuthStore();
74+
store.me = { role: e_player_roles_enum.streamer } as any;
75+
expect(store.isRoleAbove(e_player_roles_enum.streamer)).toBe(true);
76+
});
77+
78+
it("respects full hierarchy: user < verified_user < streamer < match_organizer < tournament_organizer < administrator", () => {
79+
const store = useAuthStore();
80+
const hierarchy = [
81+
e_player_roles_enum.user,
82+
e_player_roles_enum.verified_user,
83+
e_player_roles_enum.streamer,
84+
e_player_roles_enum.match_organizer,
85+
e_player_roles_enum.tournament_organizer,
86+
e_player_roles_enum.administrator,
87+
];
88+
89+
for (let i = 0; i < hierarchy.length; i++) {
90+
store.me = { role: hierarchy[i] } as any;
91+
for (let j = 0; j < hierarchy.length; j++) {
92+
expect(store.isRoleAbove(hierarchy[j])).toBe(i >= j);
93+
}
94+
}
95+
});
96+
});
97+
98+
describe("computed role checks", () => {
99+
it("isAdmin true only for administrator", () => {
100+
const store = useAuthStore();
101+
store.me = { role: e_player_roles_enum.administrator } as any;
102+
expect(store.isAdmin).toBe(true);
103+
store.me = { role: e_player_roles_enum.user } as any;
104+
expect(store.isAdmin).toBe(false);
105+
});
106+
107+
it("isUser true only for user role", () => {
108+
const store = useAuthStore();
109+
store.me = { role: e_player_roles_enum.user } as any;
110+
expect(store.isUser).toBe(true);
111+
store.me = { role: e_player_roles_enum.administrator } as any;
112+
expect(store.isUser).toBe(false);
113+
});
114+
115+
it("isMatchOrganizer true only for match_organizer", () => {
116+
const store = useAuthStore();
117+
store.me = { role: e_player_roles_enum.match_organizer } as any;
118+
expect(store.isMatchOrganizer).toBe(true);
119+
store.me = { role: e_player_roles_enum.user } as any;
120+
expect(store.isMatchOrganizer).toBe(false);
121+
});
122+
});
123+
});

stores/SearchStore.spec.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { createPinia, setActivePinia } from "pinia";
2+
3+
const playersOnline = ref<any[]>([]);
4+
5+
vi.mock("~/stores/MatchmakingStore", () => ({
6+
useMatchmakingStore: () => ({
7+
playersOnline: playersOnline.value,
8+
}),
9+
}));
10+
11+
import { useSearchStore } from "./SearchStore";
12+
13+
const makePlayers = (names: string[]) =>
14+
names.map((name, i) => ({
15+
steam_id: `steam-${i}`,
16+
name,
17+
avatar_url: "",
18+
country: "US",
19+
is_banned: false,
20+
is_muted: false,
21+
is_gagged: false,
22+
}));
23+
24+
describe("SearchStore", () => {
25+
beforeEach(() => {
26+
setActivePinia(createPinia());
27+
playersOnline.value = [];
28+
localStorage.removeItem("playerSearchOnlineOnly");
29+
});
30+
31+
it("empty query returns first 10 players from playersOnline", () => {
32+
playersOnline.value = makePlayers(Array.from({ length: 15 }, (_, i) => `Player${i}`));
33+
const store = useSearchStore();
34+
const results = store.search("", []);
35+
expect(results).toHaveLength(10);
36+
});
37+
38+
it("empty query excludes players in exclude list", () => {
39+
playersOnline.value = makePlayers(["Alice", "Bob", "Charlie"]);
40+
const store = useSearchStore();
41+
const results = store.search("", ["steam-1"]); // exclude Bob
42+
expect(results).toHaveLength(2);
43+
expect(results.find((r: any) => r.name === "Bob")).toBeUndefined();
44+
});
45+
46+
it("search query returns fuzzy-matched results", () => {
47+
playersOnline.value = makePlayers(["Alice", "Bob", "Charlie"]);
48+
const store = useSearchStore();
49+
const results = store.search("Alice", []);
50+
expect(results.length).toBeGreaterThan(0);
51+
expect(results[0].name).toBe("Alice");
52+
});
53+
54+
it("search excludes players in exclude list", () => {
55+
playersOnline.value = makePlayers(["Alice", "Alicia", "Bob"]);
56+
const store = useSearchStore();
57+
const results = store.search("Ali", ["steam-0"]); // exclude Alice
58+
expect(results.find((r: any) => r.name === "Alice")).toBeUndefined();
59+
});
60+
61+
it("onlineOnly defaults to true", () => {
62+
const store = useSearchStore();
63+
expect(store.onlineOnly).toBe(true);
64+
});
65+
66+
it("onlineOnly reads from localStorage", () => {
67+
localStorage.setItem("playerSearchOnlineOnly", "false");
68+
const store = useSearchStore();
69+
expect(store.onlineOnly).toBe(false);
70+
});
71+
});

0 commit comments

Comments
 (0)