fix(panel): 我的提交页轮询审核与构建进度,构建进行中走快档,重读失败保留列表并提示

This commit is contained in:
Lemon-miaow committed 2026-09-27 15:38:10 +08:00
1 parent c0228f4e86
commit d13d99ed75
2 files changed
+71 -4

No files matched your search

+61 -1
View File
@@ -1,10 +1,11 @@
// @vitest-environment jsdom
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen } from "@testing-library/react";
import { act, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MemoryRouter } from "react-router-dom";
import i18next from "i18next";
import { MySubmissionsPage } from "./MySubmissionsPage";
import { humanizeError } from "@/lib/api";
import type { SubmissionPage } from "@/lib/types";
const calls = vi.hoisted(() => ({
@@ -49,6 +50,7 @@ beforeEach(() => {
);
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
return i18next.changeLanguage("en-US");
});
@@ -209,6 +211,64 @@ describe("MySubmissionsPage", () => {
});
});
describe("MySubmissionsPage keeps reading", () => {
const row = PAGE.submissions[0];
const waiting: SubmissionPage = {
submissions: [{ ...row, status: "pending_review" }],
total: 1,
counts: { pending_review: 1, approved: 0, rejected: 0 },
};
const approved: SubmissionPage = {
submissions: [{ ...row, status: "approved" }],
total: 1,
counts: { pending_review: 0, approved: 1, rejected: 0 },
};
const renderPage = () =>
render(
<MemoryRouter>
<MySubmissionsPage />
</MemoryRouter>,
);
it("picks up a review verdict at the slow pace", async () => {
vi.useFakeTimers({ shouldAdvanceTime: true });
calls.listMySubmissions.mockResolvedValue(waiting);
renderPage();
await screen.findByRole("button", { name: "Pending Review (1)" });
const before = calls.listMySubmissions.mock.calls.length;
calls.listMySubmissions.mockResolvedValue(approved);
await act(() => vi.advanceTimersByTimeAsync(14_000));
expect(calls.listMySubmissions.mock.calls.length).toBe(before);
await act(() => vi.advanceTimersByTimeAsync(1_000));
expect(calls.listMySubmissions.mock.calls.length).toBe(before + 1);
expect(await screen.findByRole("button", { name: "Approved (1)" })).toBeTruthy();
});
it.each(["pending", "building"] as const)("rereads fast while a shown build is %s", async (build_status) => {
vi.useFakeTimers({ shouldAdvanceTime: true });
calls.listMySubmissions.mockResolvedValue({ ...approved, submissions: [{ ...row, build_id: "b-1", build_status }] });
renderPage();
await screen.findByText("Create Above and Beyond");
const before = calls.listMySubmissions.mock.calls.length;
await act(() => vi.advanceTimersByTimeAsync(4_000));
expect(calls.listMySubmissions.mock.calls.length).toBe(before + 1);
});
it("keeps the list when a later read fails, and says why", async () => {
vi.useFakeTimers({ shouldAdvanceTime: true });
calls.listMySubmissions.mockResolvedValue(waiting);
renderPage();
await screen.findByRole("button", { name: "Pending Review (1)" });
const outage = { status: 503, code: "unavailable", message: "list unavailable" };
calls.listMySubmissions.mockRejectedValue(outage);
await act(() => vi.advanceTimersByTimeAsync(15_000));
expect((await screen.findByRole("alert")).textContent).toContain(humanizeError(outage));
expect(screen.getByText("Create Above and Beyond")).toBeTruthy();
expect(screen.getByRole("button", { name: "Pending Review (1)" })).toBeTruthy();
});
});
// openFormWith opens the submit dialog, names the pack and picks a 4 KiB file.
async function openFormWith(name: string): Promise<File> {
render(
+10 -3
View File
@@ -32,14 +32,14 @@ import {
import { cn } from "@/lib/utils";
import { MessageLine } from "@/components/MessageLine";
import { InlineConfirm } from "@/components/InlineConfirm";
import { Loading, ErrorState, EmptyState } from "@/components/States";
import { Loading, ErrorState, EmptyState, RefreshError } from "@/components/States";
import { Pagination } from "@/components/Pagination";
import { StatCard } from "@/components/StatCard";
import { PageHeader } from "@/components/PageHeader";
import { SubmissionStatusBadge } from "@/components/SubmissionStatusBadge";
import { api, humanizeError } from "@/lib/api";
import { uploadContext } from "@/lib/contextUpload";
import { useAsync } from "@/lib/hooks";
import { STATUS_POLL_FAST_MS, STATUS_POLL_SLOW_MS, useAsync, usePolling } from "@/lib/hooks";
import { formatRelative, formatAbsolute } from "@/lib/format";
import type { BuildStatus, Submission, SubmissionStatus } from "@/lib/types";
@@ -134,6 +134,11 @@ export function MySubmissionsPage() {
const { data, error: fetchError, loading, reload } = useAsync(listMine, [listMine], { keepPrevious: true });
const submissions = useMemo<Submission[]>(() => data?.submissions ?? [], [data]);
const matching = data?.total ?? 0;
// A review lands whenever an admin gets to it and an approved pack builds on
// its own, so the list keeps reading: every few seconds while a shown build
// is still under way, at the slow pace otherwise.
const building = submissions.some((s) => s.build_status === "pending" || s.build_status === "building");
usePolling(reload, building ? STATUS_POLL_FAST_MS : STATUS_POLL_SLOW_MS);
// The cards and filter chips count everything this player submitted, whatever is filtered.
const stats = useMemo(() => {
@@ -316,6 +321,8 @@ export function MySubmissionsPage() {
{/* Row-action error (withdraw) */}
{actionError && <MessageLine kind="error" message={actionError} compact />}
{/* A later read that failed keeps the last list and says so above it. */}
{!!fetchError && !!data && <RefreshError error={fetchError} />}
{/* Stats Cards Row */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-4">
@@ -398,7 +405,7 @@ export function MySubmissionsPage() {
{/* List Content */}
{loading && !data ? (
<div className="py-12"><Loading /></div>
) : fetchError ? (
) : fetchError && !data ? (
<div className="py-12"><ErrorState error={fetchError} onRetry={reload} /></div>
) : submissions.length === 0 ? (
<div className="p-4 border-b-0">