diff --git a/panel/src/pages/MySubmissionsPage.test.tsx b/panel/src/pages/MySubmissionsPage.test.tsx
index afd1bbf..c680906 100644
--- a/panel/src/pages/MySubmissionsPage.test.tsx
+++ b/panel/src/pages/MySubmissionsPage.test.tsx
@@ -1,6 +1,6 @@
// @vitest-environment jsdom
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-import { act, render, screen } from "@testing-library/react";
+import { act, fireEvent, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MemoryRouter } from "react-router-dom";
import i18next from "i18next";
@@ -78,6 +78,26 @@ describe("MySubmissionsPage", () => {
await vi.waitFor(() => expect(screen.queryByText(/^Page \d+ of/)).toBeNull());
});
+ // The clock moves only when the test moves it, so the page is turned before
+ // the search box's pause has run out however fast the machine is.
+ it("keeps a page turned to right after the list loads", async () => {
+ vi.useFakeTimers();
+ render(
+
+
+ ,
+ );
+ await act(() => vi.advanceTimersByTimeAsync(0));
+ expect(screen.getByText("Page 1 of 2")).toBeTruthy();
+
+ fireEvent.click(screen.getByRole("button", { name: "Next" }));
+ await act(() => vi.advanceTimersByTimeAsync(0));
+ expect(calls.listMySubmissions).toHaveBeenLastCalledWith({ limit: 10, offset: 10 });
+ await act(() => vi.advanceTimersByTimeAsync(1_000));
+ expect(calls.listMySubmissions).toHaveBeenLastCalledWith({ limit: 10, offset: 10 });
+ expect(screen.getByText("Page 2 of 2")).toBeTruthy();
+ });
+
it("refuses a file over the server's per-upload cap before sending it", async () => {
render(
diff --git a/panel/src/pages/MySubmissionsPage.tsx b/panel/src/pages/MySubmissionsPage.tsx
index 1ea8994..c274cbb 100644
--- a/panel/src/pages/MySubmissionsPage.tsx
+++ b/panel/src/pages/MySubmissionsPage.tsx
@@ -113,13 +113,17 @@ export function MySubmissionsPage() {
const [statusFilter, setStatusFilter] = useState<"all" | SubmissionStatus>("all");
const [page, setPage] = useState(1);
const [expandedId, setExpandedId] = useState(null);
+ // A search already asked (the first render's included) starts no timer, so
+ // a page turned just after the list loads is not sent back to page one.
useEffect(() => {
+ const next = search.trim();
+ if (next === query) return;
const timer = setTimeout(() => {
- setQuery(search.trim());
+ setQuery(next);
setPage(1);
}, SEARCH_DEBOUNCE_MS);
return () => clearTimeout(timer);
- }, [search]);
+ }, [search, query]);
const listMine = useCallback(
() =>
diff --git a/panel/src/pages/admin/ImageBuildPage.test.tsx b/panel/src/pages/admin/ImageBuildPage.test.tsx
index 368003d..c5d5cd7 100644
--- a/panel/src/pages/admin/ImageBuildPage.test.tsx
+++ b/panel/src/pages/admin/ImageBuildPage.test.tsx
@@ -1,6 +1,6 @@
// @vitest-environment jsdom
import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from "vitest";
-import { render, screen, within } from "@testing-library/react";
+import { act, fireEvent, render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MemoryRouter } from "react-router-dom";
import i18next from "i18next";
@@ -100,6 +100,21 @@ describe("ImageBuildPage list", () => {
await vi.waitFor(() => expect(calls.listBuilds).toHaveBeenLastCalledWith({ query: undefined, limit: 10, offset: 10 }));
});
+ // The clock moves only when the test moves it, so the page is turned before
+ // the search box's pause has run out however fast the machine is.
+ it("keeps a page turned to right after the list loads", async () => {
+ vi.useFakeTimers();
+ calls.listBuilds.mockResolvedValue(page([MINE], 25));
+ renderPage();
+ await act(() => vi.advanceTimersByTimeAsync(0));
+ expect(screen.getByText(MINE.image_ref)).toBeTruthy();
+ fireEvent.click(screen.getByRole("button", { name: t("common:pagination_next") }));
+ await act(() => vi.advanceTimersByTimeAsync(0));
+ expect(calls.listBuilds).toHaveBeenLastCalledWith({ query: undefined, limit: 10, offset: 10 });
+ await act(() => vi.advanceTimersByTimeAsync(1_000));
+ expect(calls.listBuilds).toHaveBeenLastCalledWith({ query: undefined, limit: 10, offset: 10 });
+ });
+
it("searches on the server, sending a status typed as the badge shows it by its code, from page one", async () => {
await i18next.changeLanguage("zh-CN");
calls.listBuilds.mockResolvedValue(page([MINE], 25));
diff --git a/panel/src/pages/admin/ImageBuildPage.tsx b/panel/src/pages/admin/ImageBuildPage.tsx
index cef151a..8760129 100644
--- a/panel/src/pages/admin/ImageBuildPage.tsx
+++ b/panel/src/pages/admin/ImageBuildPage.tsx
@@ -158,13 +158,17 @@ export function ImageBuildPage() {
const [search, setSearch] = useState("");
const [query, setQuery] = useState("");
const [page, setPage] = useState(1);
+ // A search already asked (the first render's included) starts no timer, so
+ // a page turned just after the list loads is not sent back to page one.
useEffect(() => {
+ const next = serverQuery(search, t);
+ if (next === query) return;
const timer = setTimeout(() => {
- setQuery(serverQuery(search, t));
+ setQuery(next);
setPage(1);
}, SEARCH_DEBOUNCE_MS);
return () => clearTimeout(timer);
- }, [search, t]);
+ }, [search, t, query]);
const listBuilds = useCallback(
() => api.listBuilds({ query: query || undefined, limit: PAGE_SIZE, offset: (page - 1) * PAGE_SIZE }),
diff --git a/panel/src/pages/admin/SubmissionsPage.test.tsx b/panel/src/pages/admin/SubmissionsPage.test.tsx
index 3564437..ca581f9 100644
--- a/panel/src/pages/admin/SubmissionsPage.test.tsx
+++ b/panel/src/pages/admin/SubmissionsPage.test.tsx
@@ -1,6 +1,6 @@
// @vitest-environment jsdom
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-import { render, screen } from "@testing-library/react";
+import { act, fireEvent, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MemoryRouter } from "react-router-dom";
import i18next from "i18next";
@@ -38,6 +38,7 @@ beforeEach(() => {
calls.listSubmissions.mockResolvedValue(PAGE);
});
afterEach(() => {
+ vi.useRealTimers();
vi.restoreAllMocks();
return i18next.changeLanguage("en-US");
});
@@ -84,6 +85,21 @@ describe("SubmissionsPage", () => {
);
});
+ // The clock moves only when the test moves it, so the page is turned before
+ // the search box's pause has run out however fast the machine is.
+ it("keeps a page turned to right after the queue loads", async () => {
+ vi.useFakeTimers();
+ renderPage();
+ await act(() => vi.advanceTimersByTimeAsync(0));
+ expect(screen.getByText("Page 1 of 3")).toBeTruthy();
+ fireEvent.click(screen.getByRole("button", { name: "Next" }));
+ await act(() => vi.advanceTimersByTimeAsync(0));
+ expect(calls.listSubmissions).toHaveBeenLastCalledWith({ limit: 10, offset: 10 });
+ await act(() => vi.advanceTimersByTimeAsync(1_000));
+ expect(calls.listSubmissions).toHaveBeenLastCalledWith({ limit: 10, offset: 10 });
+ expect(screen.getByText("Page 2 of 3")).toBeTruthy();
+ });
+
it("steps back to the last page that still has rows when its own page comes back empty", async () => {
// Reviews elsewhere shrank the queue to 20 rows: page 3 no longer exists.
calls.listSubmissions.mockImplementation(async ({ offset }: { offset: number }) =>
diff --git a/panel/src/pages/admin/SubmissionsPage.tsx b/panel/src/pages/admin/SubmissionsPage.tsx
index bf18fb7..6eef6b3 100644
--- a/panel/src/pages/admin/SubmissionsPage.tsx
+++ b/panel/src/pages/admin/SubmissionsPage.tsx
@@ -60,13 +60,17 @@ export function SubmissionsPage() {
const [statusFilter, setStatusFilter] = useState<"all" | SubmissionStatus>("all");
const [page, setPage] = useState(1);
const [expandedId, setExpandedId] = useState(null);
+ // A search already asked (the first render's included) starts no timer, so
+ // a page turned just after the queue loads is not sent back to page one.
useEffect(() => {
+ const next = search.trim();
+ if (next === query) return;
const timer = setTimeout(() => {
- setQuery(search.trim());
+ setQuery(next);
setPage(1);
}, SEARCH_DEBOUNCE_MS);
return () => clearTimeout(timer);
- }, [search]);
+ }, [search, query]);
const listSubmissions = useCallback(
() =>