Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
dd13698
refactor: solid leaderboards (@fehmer)
fehmer Jan 31, 2026
49c92d3
using collections, not the best
fehmer Feb 1, 2026
5b8d530
fetch dynamic leaderboard from server configuration
fehmer Feb 2, 2026
9e5cbc4
replace db with query
fehmer Feb 2, 2026
dfb7fb3
fix friends not filtered
fehmer Feb 2, 2026
018aebc
hide columns depending on screen size, typing speed in lb
fehmer Feb 2, 2026
e1047f3
wip
fehmer Feb 3, 2026
9fec8bb
wip
fehmer Feb 3, 2026
feac67c
don't preload if not visible
fehmer Feb 3, 2026
77af738
wip
fehmer Feb 4, 2026
800bcfd
cleanup
fehmer Feb 4, 2026
82eafe4
wip
fehmer Feb 6, 2026
94d85db
refactor
fehmer Feb 6, 2026
e3517aa
move stuff around
fehmer Feb 6, 2026
2389d80
add rank query, scroll to user
fehmer Feb 6, 2026
d216579
loading indicator on rank
fehmer Feb 7, 2026
d147b93
add rank info
fehmer Feb 8, 2026
9353de8
simplify rank table
fehmer Feb 8, 2026
6ba3635
lb memory, not the bestg
fehmer Feb 8, 2026
2f29d7e
move server-configuration query
fehmer Feb 9, 2026
15d5b44
add url params
fehmer Feb 9, 2026
2faf95b
use serverConfigurationQuery, pass validModeRule to sidebar
fehmer Feb 10, 2026
b48175d
add minTyping, banned, optOut to UserRank
fehmer Feb 10, 2026
3fa0e22
add (skeleton) page back to the page file, simplifies url parameter h…
fehmer Feb 10, 2026
82228ff
xp header
fehmer Feb 10, 2026
471ed1a
fix missing rank when data query is not complete
fehmer Feb 10, 2026
b3a4f22
remove resource from AyncContent, add multi query
fehmer Feb 11, 2026
2c17328
xp lb combined columns
fehmer Feb 11, 2026
68d91df
cleanup
fehmer Feb 11, 2026
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
301 changes: 239 additions & 62 deletions frontend/__tests__/components/common/AsyncContent.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { render, screen, waitFor } from "@solidjs/testing-library";
import { createResource, Resource, Show } from "solid-js";
import {
QueryClient,
QueryClientProvider,
useQuery,
} from "@tanstack/solid-query";
import { JSXElement, Show } from "solid-js";
import { beforeEach, describe, expect, it, vi } from "vitest";

import AsyncContent from "../../../src/ts/components/common/AsyncContent";
Expand All @@ -11,14 +16,12 @@ describe("AsyncContent", () => {
beforeEach(() => {
addNotificationMock.mockClear();
});
describe("with resource", () => {
it("renders loading state while resource is pending", () => {
const [resource] = createResource(async () => {
await new Promise((resolve) => setTimeout(resolve, 100));
return "data";
});

const { container } = renderWithResource(resource);
describe("with single query", () => {
const queryClient = new QueryClient();

it("renders loading state while pending", () => {
const { container } = renderWithQuery({ result: "data" });

const preloader = container.querySelector(".preloader");
expect(preloader).toBeInTheDocument();
Expand All @@ -31,24 +34,64 @@ describe("AsyncContent", () => {
);
});

it("renders data when resource resolves", async () => {
const [resource] = createResource(async () => {
return "Test Data";
it("renders on resolve", async () => {
renderWithQuery({ result: "Test Data" });

await waitFor(() => {
expect(screen.getByTestId("content")).toHaveTextContent("Test Data");
});
});

renderWithResource(resource);
it("renders default error message on fail", async () => {
renderWithQuery({ result: new Error("Test error") });

await waitFor(() => {
expect(screen.getByText(/An error occurred/)).toBeInTheDocument();
});
expect(addNotificationMock).toHaveBeenCalledWith(
"An error occurred: Test error",
-1,
);
});

it("renders custom error message on fail", async () => {
renderWithQuery(
{ result: new Error("Test error") },
{ errorMessage: "Custom error message" },
);

await waitFor(() => {
expect(screen.getByText(/Custom error message/)).toBeInTheDocument();
});
expect(addNotificationMock).toHaveBeenCalledWith(
"Custom error message: Test error",
-1,
);
});

it("renders on pending if alwaysShowContent", async () => {
const { container } = renderWithQuery({ result: "Test Data" });

await waitFor(() => {
expect(screen.getByTestId("content")).toHaveTextContent("Test Data");
});
const preloader = container.querySelector(".preloader");
expect(preloader).not.toBeInTheDocument();
});

it("renders error message when resource fails", async () => {
const [resource] = createResource(async () => {
throw new Error("Test error");
it("renders on resolve if alwaysShowContent", async () => {
renderWithQuery({ result: "Test Data" }, { alwaysShowContent: true });

await waitFor(() => {
expect(screen.getByTestId("content")).toHaveTextContent("Test Data");
});
});

renderWithResource(resource, "Custom error message");
it("renders on fail if alwaysShowContent", async () => {
renderWithQuery(
{ result: new Error("Test error") },
{ errorMessage: "Custom error message" },
);

await waitFor(() => {
expect(screen.getByText(/Custom error message/)).toBeInTheDocument();
Expand All @@ -59,12 +102,89 @@ describe("AsyncContent", () => {
);
});

it("renders default error message when no custom message provided", async () => {
const [resource] = createResource(async () => {
throw new Error("Test error");
function renderWithQuery(
query: {
result: string | Error;
},
options?: {
errorMessage?: string;
alwaysShowContent?: true;
},
): {
container: HTMLElement;
} {
const wrapper = (): JSXElement => {
const myQuery = useQuery(() => ({
queryKey: ["test", Math.random() * 1000],
queryFn: async () => {
await new Promise((resolve) => setTimeout(resolve, 100));
if (query.result instanceof Error) {
throw query.result;
}
return query.result;
},
retry: 0,
}));

return (
<AsyncContent
query={myQuery}
errorMessage={options?.errorMessage}
alwaysShowContent={options?.alwaysShowContent}
>
{(data: string | undefined) => (
<>
foo
<Show when={data !== undefined} fallback={<div>no data</div>}>
<div data-testid="content">{data}</div>
</Show>
</>
)}
</AsyncContent>
);
};
const { container } = render(() => (
<QueryClientProvider client={queryClient}>
{wrapper()}
</QueryClientProvider>
));

return {
container,
};
}
});

describe("with multiple queries", () => {
const queryClient = new QueryClient();

it("renders loading state while pending", () => {
const { container } = renderWithQuery({ first: "data", second: "data" });

const preloader = container.querySelector(".preloader");
expect(preloader).toBeInTheDocument();
expect(preloader).toHaveClass("preloader");
expect(preloader?.querySelector("i")).toHaveClass(
"fas",
"fa-fw",
"fa-spin",
"fa-circle-notch",
);
});

it("renders on resolve", async () => {
renderWithQuery({ first: "First Data", second: "Second Data" });

await waitFor(() => {
expect(screen.getByTestId("first")).toHaveTextContent("First Data");
});
await waitFor(() => {
expect(screen.getByTestId("second")).toHaveTextContent("Second Data");
});
});

renderWithResource(resource);
it("renders default error message on fail", async () => {
renderWithQuery({ first: "data", second: new Error("Test error") });

await waitFor(() => {
expect(screen.getByText(/An error occurred/)).toBeInTheDocument();
Expand All @@ -75,42 +195,56 @@ describe("AsyncContent", () => {
);
});

it("renders content while resource is pending if alwaysShowContent", () => {
const [resource] = createResource(async () => {
await new Promise((resolve) => setTimeout(resolve, 100));
return "data";
it("renders custom error message on fail", async () => {
renderWithQuery(
{ first: new Error("Test error"), second: new Error("Test error") },
{ errorMessage: "Custom error message" },
);

await waitFor(() => {
expect(screen.getByText(/Custom error message/)).toBeInTheDocument();
});
expect(addNotificationMock).toHaveBeenCalledWith(
"Custom error message: Test error",
-1,
);
});

const { container } = renderWithResource(resource, undefined, true);
it("renders on pending if alwaysShowContent", async () => {
const { container } = renderWithQuery(
{
first: undefined,
second: undefined,
},
{ alwaysShowContent: true },
);

const preloader = container.querySelector(".preloader");
expect(preloader).not.toBeInTheDocument();
expect(container.querySelector("div")).toHaveTextContent("no data");
});

it("renders data when resource resolves if alwaysShowContent", async () => {
const [resource] = createResource(async () => {
return "Test Data";
await waitFor(() => {
expect(screen.getByText(/no data/)).toBeInTheDocument();
});
});

const { container } = renderWithResource(resource, undefined, true);
it("renders on resolve if alwaysShowContent", async () => {
renderWithQuery({
first: "First Data",
second: "Second Data",
});

await waitFor(() => {
expect(screen.getByTestId("content")).toHaveTextContent("Test Data");
expect(screen.getByTestId("first")).toHaveTextContent("First Data");
});
const preloader = container.querySelector(".preloader");
expect(preloader).not.toBeInTheDocument();
});

it("renders error message when resource fails if alwaysShowContent", async () => {
const [resource] = createResource(async () => {
throw new Error("Test error");
await waitFor(() => {
expect(screen.getByTestId("second")).toHaveTextContent("Second Data");
});
});

const { container } = renderWithResource(
resource,
"Custom error message",
true,
it("renders on fail if alwaysShowContent", async () => {
renderWithQuery(
{ first: "data", second: new Error("Test error") },
{ errorMessage: "Custom error message" },
);

await waitFor(() => {
Expand All @@ -120,30 +254,73 @@ describe("AsyncContent", () => {
"Custom error message: Test error",
-1,
);
console.log(container.innerHTML);
});
function renderWithResource<T>(
resource: Resource<T>,
errorMessage?: string,
alwaysShowContent?: true,

function renderWithQuery(
queries: {
first: string | Error | undefined;
second: string | Error | undefined;
},
options?: {
errorMessage?: string;
alwaysShowContent?: true;
},
): {
container: HTMLElement;
} {
const wrapper = (): JSXElement => {
const firstQuery = useQuery(() => ({
queryKey: ["first", Math.random() * 1000],
queryFn: async () => {
await new Promise((resolve) => setTimeout(resolve, 100));
if (queries.first instanceof Error) {
throw queries.first;
}
return queries.first;
},
retry: 0,
}));
const secondQuery = useQuery(() => ({
queryKey: ["second", Math.random() * 1000],
queryFn: async () => {
await new Promise((resolve) => setTimeout(resolve, 100));
if (queries.second instanceof Error) {
throw queries.second;
}
return queries.second;
},
retry: 0,
}));

return (
<AsyncContent
queries={{ first: firstQuery, second: secondQuery }}
errorMessage={options?.errorMessage}
alwaysShowContent={options?.alwaysShowContent}
>
{(results: {
first: string | undefined;
second: string | undefined;
}) => (
<>
<Show
when={
results.first !== undefined && results.second !== undefined
}
fallback={<div>no data</div>}
>
<div data-testid="first">{results.first}</div>
<div data-testid="second">{results.second}</div>
</Show>
</>
)}
</AsyncContent>
);
};
const { container } = render(() => (
<AsyncContent
resource={resource}
errorMessage={errorMessage}
alwaysShowContent={alwaysShowContent}
>
{(data: T | undefined) => (
<>
foo
<Show when={data !== undefined} fallback={<div>no data</div>}>
<div data-testid="content">{String(data)}</div>
</Show>
</>
)}
</AsyncContent>
<QueryClientProvider client={queryClient}>
{wrapper()}
</QueryClientProvider>
));

return {
Expand Down
Loading