fix(panel): 我的提交页轮询审核与构建进度,构建进行中走快档,重读失败保留列表并提示
This commit is contained in:
2 files changed
+71
-4
No files matched your search
@@ -1,10 +1,11 @@
|
|||||||
// @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, 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";
|
||||||
import { MySubmissionsPage } from "./MySubmissionsPage";
|
import { MySubmissionsPage } from "./MySubmissionsPage";
|
||||||
|
import { humanizeError } from "@/lib/api";
|
||||||
import type { SubmissionPage } from "@/lib/types";
|
import type { SubmissionPage } from "@/lib/types";
|
||||||
|
|
||||||
const calls = vi.hoisted(() => ({
|
const calls = vi.hoisted(() => ({
|
||||||
@@ -49,6 +50,7 @@ beforeEach(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
return i18next.changeLanguage("en-US");
|
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.
|
// openFormWith opens the submit dialog, names the pack and picks a 4 KiB file.
|
||||||
async function openFormWith(name: string): Promise<File> {
|
async function openFormWith(name: string): Promise<File> {
|
||||||
render(
|
render(
|
||||||
|
|||||||
@@ -32,14 +32,14 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { MessageLine } from "@/components/MessageLine";
|
import { MessageLine } from "@/components/MessageLine";
|
||||||
import { InlineConfirm } from "@/components/InlineConfirm";
|
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 { Pagination } from "@/components/Pagination";
|
||||||
import { StatCard } from "@/components/StatCard";
|
import { StatCard } from "@/components/StatCard";
|
||||||
import { PageHeader } from "@/components/PageHeader";
|
import { PageHeader } from "@/components/PageHeader";
|
||||||
import { SubmissionStatusBadge } from "@/components/SubmissionStatusBadge";
|
import { SubmissionStatusBadge } from "@/components/SubmissionStatusBadge";
|
||||||
import { api, humanizeError } from "@/lib/api";
|
import { api, humanizeError } from "@/lib/api";
|
||||||
import { uploadContext } from "@/lib/contextUpload";
|
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 { formatRelative, formatAbsolute } from "@/lib/format";
|
||||||
import type { BuildStatus, Submission, SubmissionStatus } from "@/lib/types";
|
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 { data, error: fetchError, loading, reload } = useAsync(listMine, [listMine], { keepPrevious: true });
|
||||||
const submissions = useMemo<Submission[]>(() => data?.submissions ?? [], [data]);
|
const submissions = useMemo<Submission[]>(() => data?.submissions ?? [], [data]);
|
||||||
const matching = data?.total ?? 0;
|
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.
|
// The cards and filter chips count everything this player submitted, whatever is filtered.
|
||||||
const stats = useMemo(() => {
|
const stats = useMemo(() => {
|
||||||
@@ -316,6 +321,8 @@ export function MySubmissionsPage() {
|
|||||||
|
|
||||||
{/* Row-action error (withdraw) */}
|
{/* Row-action error (withdraw) */}
|
||||||
{actionError && <MessageLine kind="error" message={actionError} compact />}
|
{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 */}
|
{/* Stats Cards Row */}
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-4">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-4">
|
||||||
@@ -398,7 +405,7 @@ export function MySubmissionsPage() {
|
|||||||
{/* List Content */}
|
{/* List Content */}
|
||||||
{loading && !data ? (
|
{loading && !data ? (
|
||||||
<div className="py-12"><Loading /></div>
|
<div className="py-12"><Loading /></div>
|
||||||
) : fetchError ? (
|
) : fetchError && !data ? (
|
||||||
<div className="py-12"><ErrorState error={fetchError} onRetry={reload} /></div>
|
<div className="py-12"><ErrorState error={fetchError} onRetry={reload} /></div>
|
||||||
) : submissions.length === 0 ? (
|
) : submissions.length === 0 ? (
|
||||||
<div className="p-4 border-b-0">
|
<div className="p-4 border-b-0">
|
||||||
|
|||||||
Reference in new issue
Block a user