fix(panel): 搜索框的停顿计时只在搜索词变了才起,列表刚加载就翻的页不再被拉回第一页
This commit is contained in:
6 files changed
+72
-9
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
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 userEvent from "@testing-library/user-event";
|
||||||
import { MemoryRouter } from "react-router-dom";
|
import { MemoryRouter } from "react-router-dom";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
@@ -78,6 +78,26 @@ describe("MySubmissionsPage", () => {
|
|||||||
await vi.waitFor(() => expect(screen.queryByText(/^Page \d+ of/)).toBeNull());
|
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(
|
||||||
|
<MemoryRouter>
|
||||||
|
<MySubmissionsPage />
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
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 () => {
|
it("refuses a file over the server's per-upload cap before sending it", async () => {
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
|
|||||||
@@ -113,13 +113,17 @@ export function MySubmissionsPage() {
|
|||||||
const [statusFilter, setStatusFilter] = useState<"all" | SubmissionStatus>("all");
|
const [statusFilter, setStatusFilter] = useState<"all" | SubmissionStatus>("all");
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
const [expandedId, setExpandedId] = useState<string | null>(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(() => {
|
useEffect(() => {
|
||||||
|
const next = search.trim();
|
||||||
|
if (next === query) return;
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setQuery(search.trim());
|
setQuery(next);
|
||||||
setPage(1);
|
setPage(1);
|
||||||
}, SEARCH_DEBOUNCE_MS);
|
}, SEARCH_DEBOUNCE_MS);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [search]);
|
}, [search, query]);
|
||||||
|
|
||||||
const listMine = useCallback(
|
const listMine = useCallback(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from "vitest";
|
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 userEvent from "@testing-library/user-event";
|
||||||
import { MemoryRouter } from "react-router-dom";
|
import { MemoryRouter } from "react-router-dom";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
@@ -100,6 +100,21 @@ describe("ImageBuildPage list", () => {
|
|||||||
await vi.waitFor(() => expect(calls.listBuilds).toHaveBeenLastCalledWith({ query: undefined, limit: 10, offset: 10 }));
|
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 () => {
|
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");
|
await i18next.changeLanguage("zh-CN");
|
||||||
calls.listBuilds.mockResolvedValue(page([MINE], 25));
|
calls.listBuilds.mockResolvedValue(page([MINE], 25));
|
||||||
|
|||||||
@@ -158,13 +158,17 @@ export function ImageBuildPage() {
|
|||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [page, setPage] = useState(1);
|
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(() => {
|
useEffect(() => {
|
||||||
|
const next = serverQuery(search, t);
|
||||||
|
if (next === query) return;
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setQuery(serverQuery(search, t));
|
setQuery(next);
|
||||||
setPage(1);
|
setPage(1);
|
||||||
}, SEARCH_DEBOUNCE_MS);
|
}, SEARCH_DEBOUNCE_MS);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [search, t]);
|
}, [search, t, query]);
|
||||||
|
|
||||||
const listBuilds = useCallback(
|
const listBuilds = useCallback(
|
||||||
() => api.listBuilds({ query: query || undefined, limit: PAGE_SIZE, offset: (page - 1) * PAGE_SIZE }),
|
() => api.listBuilds({ query: query || undefined, limit: PAGE_SIZE, offset: (page - 1) * PAGE_SIZE }),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
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 userEvent from "@testing-library/user-event";
|
||||||
import { MemoryRouter } from "react-router-dom";
|
import { MemoryRouter } from "react-router-dom";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
@@ -38,6 +38,7 @@ beforeEach(() => {
|
|||||||
calls.listSubmissions.mockResolvedValue(PAGE);
|
calls.listSubmissions.mockResolvedValue(PAGE);
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
return i18next.changeLanguage("en-US");
|
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 () => {
|
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.
|
// Reviews elsewhere shrank the queue to 20 rows: page 3 no longer exists.
|
||||||
calls.listSubmissions.mockImplementation(async ({ offset }: { offset: number }) =>
|
calls.listSubmissions.mockImplementation(async ({ offset }: { offset: number }) =>
|
||||||
|
|||||||
@@ -60,13 +60,17 @@ export function SubmissionsPage() {
|
|||||||
const [statusFilter, setStatusFilter] = useState<"all" | SubmissionStatus>("all");
|
const [statusFilter, setStatusFilter] = useState<"all" | SubmissionStatus>("all");
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
const [expandedId, setExpandedId] = useState<string | null>(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(() => {
|
useEffect(() => {
|
||||||
|
const next = search.trim();
|
||||||
|
if (next === query) return;
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setQuery(search.trim());
|
setQuery(next);
|
||||||
setPage(1);
|
setPage(1);
|
||||||
}, SEARCH_DEBOUNCE_MS);
|
}, SEARCH_DEBOUNCE_MS);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [search]);
|
}, [search, query]);
|
||||||
|
|
||||||
const listSubmissions = useCallback(
|
const listSubmissions = useCallback(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
Reference in new issue
Block a user