Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions app/src/__tests__/friends.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import {
loadFriends,
addFriend,
removeFriend,
setFriendAlias,
setFriendInvited,
markFriendPresence,
computeOnlineAddresses,
tablesOccupiedBy,
displayName,
shortAddr,
} from "@/lib/friends";
import type { OpenTable } from "@/lib/open-tables";

const ADDR_A = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA55";
const ADDR_B = "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB55";

function setupStorage() {
const store = new Map<string, string>();
vi.stubGlobal("localStorage", {
getItem: (key: string) => store.get(key) ?? null,
setItem: (key: string, value: string) => { store.set(key, value); },
removeItem: (key: string) => { store.delete(key); },
clear: () => store.clear(),
length: 0,
key: () => null,
});
return store;
}

describe("friends store", () => {
beforeEach(setupStorage);
afterEach(() => {
vi.unstubAllGlobals();
vi.clearAllMocks();
});

it("adds a friend and persists it", () => {
const next = addFriend(ADDR_A, "Alice");
expect(next).toHaveLength(1);
expect(next[0].address).toBe(ADDR_A);
expect(next[0].alias).toBe("Alice");
expect(loadFriends()).toHaveLength(1);
});

it("does not duplicate a friend", () => {
addFriend(ADDR_A, "Alice");
addFriend(ADDR_A, "Alice2");
expect(loadFriends()).toHaveLength(1);
});

it("removes a friend", () => {
addFriend(ADDR_A, "Alice");
addFriend(ADDR_B, "Bob");
const next = removeFriend(ADDR_A);
expect(next).toHaveLength(1);
expect(next[0].address).toBe(ADDR_B);
});

it("updates a friend alias", () => {
addFriend(ADDR_A, "Alice");
const next = setFriendAlias(ADDR_A, "Ace");
expect(next[0].alias).toBe("Ace");
});

it("marks a friend invited", () => {
addFriend(ADDR_A);
const next = setFriendInvited(ADDR_A, true);
expect(next[0].invited).toBe(true);
});

it("marks friend presence", () => {
addFriend(ADDR_A);
const next = markFriendPresence(ADDR_A, true);
expect(next[0].online).toBe(true);
});

it("trims aliases", () => {
addFriend(ADDR_A, " Some Very Long Alias That Should Be Trimmed ");
expect(loadFriends()[0].alias?.length).toBeLessThanOrEqual(16);
});
});

describe("computeOnlineAddresses", () => {
it("collects distinct seated addresses across open tables", () => {
const tables: OpenTable[] = [
{ tableId: 1, lastVisited: 1 },
{ tableId: 2, lastVisited: 2 },
];
const seatedAt = (id: number): string[] =>
id === 1 ? [ADDR_A] : [ADDR_A, ADDR_B];
const online = computeOnlineAddresses(tables, seatedAt);
expect(online.has(ADDR_A)).toBe(true);
expect(online.has(ADDR_B)).toBe(true);
});
});

describe("tablesOccupiedBy", () => {
it("maps each friend to the tables they occupy", () => {
const tables: OpenTable[] = [
{ tableId: 1, lastVisited: 1 },
{ tableId: 5, lastVisited: 2 },
];
const seatedAt = (id: number): string[] => (id === 1 ? [ADDR_A] : [ADDR_A, ADDR_B]);
const occupied = tablesOccupiedBy(tables, seatedAt);
expect(occupied[ADDR_A]).toEqual([1, 5]);
expect(occupied[ADDR_B]).toEqual([5]);
});
});

describe("displayName / shortAddr", () => {
it("uses alias when present", () => {
expect(displayName({ address: ADDR_A, alias: "Alice", online: false, invited: false })).toBe("Alice");
});

it("falls back to short address", () => {
expect(displayName({ address: ADDR_A, alias: null, online: false, invited: false })).toBe(shortAddr(ADDR_A));
});

it("truncates long addresses", () => {
const s = shortAddr(ADDR_A);
expect(s).toContain("…");
expect(s.length).toBeLessThan(ADDR_A.length);
});
});
122 changes: 122 additions & 0 deletions app/src/__tests__/notifications-center.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import {
loadNotifications,
pushNotification,
markAllRead,
markRead,
clearNotification,
clearAll,
unreadCount,
groupByType,
groupLabel,
NOTIFICATION_GROUPS,
fireBrowserNotificationIfHidden,
type AppNotification,
} from "@/lib/notifications-center";

function setupStorage() {
const store = new Map<string, string>();
vi.stubGlobal("localStorage", {
getItem: (key: string) => store.get(key) ?? null,
setItem: (key: string, value: string) => { store.set(key, value); },
removeItem: (key: string) => { store.delete(key); },
clear: () => store.clear(),
length: 0,
key: () => null,
});
return store;
}

describe("notifications store", () => {
beforeEach(setupStorage);
afterEach(() => {
vi.unstubAllGlobals();
vi.clearAllMocks();
});

it("pushes a notification with defaults", () => {
const next = pushNotification({ type: "friend-request", title: "Hi", body: "b" });
expect(next).toHaveLength(1);
expect(next[0].read).toBe(false);
expect(next[0].id).toBeTruthy();
expect(next[0].createdAt).toBeTruthy();
});

it("returns empty for no notifications", () => {
expect(loadNotifications()).toEqual([]);
});

it("marks all read", () => {
pushNotification({ type: "table-invite", title: "t", body: "b" });
pushNotification({ type: "achievement", title: "a", body: "b" });
const next = markAllRead();
expect(unreadCount(next)).toBe(0);
});

it("marks a single notification read by id", () => {
const [n] = pushNotification({ type: "tournament-reminder", title: "t", body: "b" });
const next = markRead(n.id);
expect(next.find((x) => x.id === n.id)?.read).toBe(true);
});

it("clears a single notification", () => {
const [n] = pushNotification({ type: "friend-request", title: "t", body: "b" });
const next = clearNotification(n.id);
expect(next).toHaveLength(0);
});

it("clears all", () => {
pushNotification({ type: "table-invite", title: "t", body: "b" });
expect(clearAll()).toEqual([]);
});

it("computes unread count", () => {
pushNotification({ type: "table-invite", title: "t", body: "b" });
pushNotification({ type: "friend-request", title: "t", body: "b" });
const items = loadNotifications();
expect(unreadCount(items)).toBe(2);
expect(unreadCount(markAllRead())).toBe(0);
});
});

describe("groupByType", () => {
it("groups notifications by type", () => {
const items: AppNotification[] = [
{ id: "1", type: "table-invite", title: "a", body: "b", createdAt: 1, read: false },
{ id: "2", type: "table-invite", title: "c", body: "d", createdAt: 2, read: false },
{ id: "3", type: "achievement", title: "e", body: "f", createdAt: 3, read: true },
];
const grouped = groupByType(items);
expect(grouped["table-invite"]).toHaveLength(2);
expect(grouped.achievement).toHaveLength(1);
expect(grouped["friend-request"]).toHaveLength(0);
expect(grouped["tournament-reminder"]).toHaveLength(0);
});

it("has an entry per group", () => {
expect(NOTIFICATION_GROUPS).toHaveLength(4);
});
});

describe("groupLabel", () => {
it("labels every type", () => {
for (const t of NOTIFICATION_GROUPS) {
expect(groupLabel(t).length).toBeGreaterThan(0);
}
});
});

describe("fireBrowserNotificationIfHidden", () => {
it("does nothing when the tab is visible", () => {
let fired = false;
(globalThis as Record<string, unknown>).document = {
hidden: false,
} as Document;
const mockCtor = vi.fn();
(globalThis as Record<string, unknown>).Notification = mockCtor as unknown as typeof Notification;
fireBrowserNotificationIfHidden({
id: "1", type: "achievement", title: "t", body: "b", createdAt: 1, read: false,
});
expect(mockCtor).not.toHaveBeenCalled();
});
});
Loading