Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
24 changes: 21 additions & 3 deletions apps/game-client/src/api/characters.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ export type MargonemCharacter = {
world?: string;
};

export const normalizeCharacterList = (
characters: unknown,
): MargonemCharacter[] => {
if (!Array.isArray(characters)) {
return [];
}

return characters.filter((character): character is MargonemCharacter => {
return (
typeof character === "object" &&
character !== null &&
typeof character.id === "number"
);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};

type FetchCharacterListOptions = {
accountId: number;
world: string | undefined;
Expand All @@ -39,9 +55,11 @@ export async function fetchCharacterList({
MargonemCharacter[]
> | null;

const cached = accountId ? (charlist?.[accountId] ?? null) : null;
const cached = accountId
? normalizeCharacterList(charlist?.[accountId] ?? null)
: [];

if (cached) {
if (cached.length > 0) {
return cached
.filter((character) => character.world === world)
.sort((a, b) => b.lvl - a.lvl);
Expand All @@ -66,7 +84,7 @@ export async function fetchCharacterList({
throw new Error("Empty character list received from API");
}

return response.data
return normalizeCharacterList(response.data)
.filter((character) => character.world === world)
.sort((a, b) => b.lvl - a.lvl);
}
12 changes: 9 additions & 3 deletions apps/game-client/src/components/delete-timer-popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import {
import { ContextMenuItem } from "@/components/ui/context-menu";
import { fetchGuildPermissions } from "@/api";
import { useGuilds } from "@/hooks/api/use-guilds";
import {
getGuildPermissionsQueryKey,
normalizeGuildPermissions,
} from "@/hooks/api/use-guild-permissions";
import type { TimerWithTimeLeft } from "@/features/timers/utils/timers-utils";
import { cn } from "@/lib/utils";
import { Permission } from "@lootlog/types";
Expand Down Expand Up @@ -42,7 +46,7 @@ export const DeleteTimerPopover: FC<DeleteTimerPopoverProps> = ({

const permissionsQueries = useQueries({
queries: uniqueGuildIds.map((guildId) => ({
queryKey: ["guild-permissions", guildId],
queryKey: getGuildPermissionsQueryKey(guildId),
queryFn: () => fetchGuildPermissions(guildId),
staleTime: 5 * 60 * 1000,
})),
Expand All @@ -51,9 +55,11 @@ export const DeleteTimerPopover: FC<DeleteTimerPopoverProps> = ({
const guildsWithPermissions = useMemo(() => {
return uniqueGuildIds
.map((guildId, index) => {
const permissions = permissionsQueries[index]?.data;
const permissions = normalizeGuildPermissions(
permissionsQueries[index]?.data,
);
const canDelete = REQUIRED_DELETE_PERMISSIONS.some((perm) =>
permissions?.includes(perm),
permissions.includes(perm),
);
const entry = guildEntries.find((e) => e.guildId === guildId);
return {
Expand Down
156 changes: 156 additions & 0 deletions apps/game-client/src/components/draggable-window.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { fireEvent, render, waitFor } from "@testing-library/react";
import { act } from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { DraggableWindow } from "@/components/draggable-window";
import { useWindowsStore } from "@/store/windows.store";

const resizeObserverCallbacks: Array<() => void> = [];

class ResizeObserverMock {
constructor(private readonly callback: () => void) {
resizeObserverCallbacks.push(callback);
}

observe() {}

disconnect() {}
}

vi.stubGlobal("ResizeObserver", ResizeObserverMock);

describe("DraggableWindow", () => {
beforeEach(() => {
resizeObserverCallbacks.length = 0;
useWindowsStore.setState((state) => ({
...state,
notifications: {
...state.notifications,
position: { x: 0, y: 0 },
size: { width: 360, height: 300 },
},
windowFocusHistory: [],
}));
});

it("caps auto height using the provided content limit", async () => {
const { container } = render(
<DraggableWindow
id="notifications"
title="Powiadomienia"
resizable={false}
minWidth={242}
minHeight={88}
heightMode="auto-up-to-max"
maxContentHeight={80}
>
<div>Treść</div>
</DraggableWindow>,
);

const windowElement = container.querySelector("#ll-notifications");

if (!(windowElement instanceof HTMLDivElement)) {
throw new Error("Expected draggable window root");
}

const windowBody = windowElement.firstElementChild;
const contentElement = windowBody?.querySelector(".ll\\:flex-1");

if (!(windowBody instanceof HTMLDivElement)) {
throw new Error("Expected draggable window body");
}

if (!(contentElement instanceof HTMLDivElement)) {
throw new Error("Expected draggable window content");
}

Object.defineProperty(windowBody, "offsetHeight", {
configurable: true,
value: 150,
});
Object.defineProperty(contentElement, "clientHeight", {
configurable: true,
value: 100,
});
Object.defineProperty(contentElement, "scrollHeight", {
configurable: true,
value: 160,
});

await act(async () => {
resizeObserverCallbacks.forEach((callback) => callback());
});

await waitFor(() => {
expect(windowElement.style.height).toBe("130px");
});
});

it("uses the resize handle to update max content height when armed", async () => {
const handleMaxContentHeightChange = vi.fn();
const handleArmedChange = vi.fn();

const { container } = render(
<DraggableWindow
id="notifications"
title="Powiadomienia"
resizable={false}
minWidth={242}
minHeight={88}
heightMode="auto-up-to-max"
maxContentHeight={80}
isMaxHeightAdjustmentArmed
onMaxContentHeightChange={handleMaxContentHeightChange}
onMaxHeightAdjustmentArmedChange={handleArmedChange}
>
<div>Treść</div>
</DraggableWindow>,
);

const windowElement = container.querySelector("#ll-notifications");

if (!(windowElement instanceof HTMLDivElement)) {
throw new Error("Expected draggable window root");
}

const windowBody = windowElement.firstElementChild;
const contentElement = windowBody?.querySelector(".ll\\:flex-1");
const resizeHandle = container.querySelector(".ll\\:cursor-se-resize");

if (!(windowBody instanceof HTMLDivElement)) {
throw new Error("Expected draggable window body");
}

if (!(contentElement instanceof HTMLDivElement)) {
throw new Error("Expected draggable window content");
}

if (!(resizeHandle instanceof HTMLDivElement)) {
throw new Error("Expected resize handle");
}

Object.defineProperty(windowElement, "offsetWidth", {
configurable: true,
value: 360,
});
Object.defineProperty(windowElement, "offsetHeight", {
configurable: true,
value: 130,
});
Object.defineProperty(windowBody, "offsetHeight", {
configurable: true,
value: 130,
});
Object.defineProperty(contentElement, "clientHeight", {
configurable: true,
value: 100,
});

fireEvent.mouseDown(resizeHandle, { clientX: 100, clientY: 100 });
fireEvent.mouseMove(document, { buttons: 1, clientX: 100, clientY: 140 });
fireEvent.mouseUp(document);

expect(handleMaxContentHeightChange).toHaveBeenCalledWith(140);
expect(handleArmedChange).toHaveBeenCalledWith(false);
});
});
Loading
Loading