Unverified Commit d13d99ed authored by Lemon-miaow's avatar Lemon-miaow
Browse files

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

parent c0228f4e
Loading
Loading
Loading
Loading
+61 −1
Changes for panel/src/pages/MySubmissionsPage.test.tsx: 61 added lines, 1 removed line.
Original line number Diff line number Diff line
// @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
Changes for panel/src/pages/MySubmissionsPage.tsx: 10 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -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">