From 6644046016181707be1bc5b076552d6ea3d57971 Mon Sep 17 00:00:00 2001 From: Lemon-miaow Date: Tue, 29 Sep 2026 03:25:26 +0800 Subject: [PATCH] =?UTF-8?q?fix(panel):=20=E6=90=9C=E7=B4=A2=E6=A1=86?= =?UTF-8?q?=E7=9A=84=E5=81=9C=E9=A1=BF=E8=AE=A1=E6=97=B6=E5=8F=AA=E5=9C=A8?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E8=AF=8D=E5=8F=98=E4=BA=86=E6=89=8D=E8=B5=B7?= =?UTF-8?q?=EF=BC=8C=E5=88=97=E8=A1=A8=E5=88=9A=E5=8A=A0=E8=BD=BD=E5=B0=B1?= =?UTF-8?q?=E7=BF=BB=E7=9A=84=E9=A1=B5=E4=B8=8D=E5=86=8D=E8=A2=AB=E6=8B=89?= =?UTF-8?q?=E5=9B=9E=E7=AC=AC=E4=B8=80=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- panel/src/pages/MySubmissionsPage.test.tsx | 22 ++++++++++++++++++- panel/src/pages/MySubmissionsPage.tsx | 8 +++++-- panel/src/pages/admin/ImageBuildPage.test.tsx | 17 +++++++++++++- panel/src/pages/admin/ImageBuildPage.tsx | 8 +++++-- .../src/pages/admin/SubmissionsPage.test.tsx | 18 ++++++++++++++- panel/src/pages/admin/SubmissionsPage.tsx | 8 +++++-- 6 files changed, 72 insertions(+), 9 deletions(-) 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( () =>