From 17482e09015ad4b388627ac8d3f1f3b72f6ef47a Mon Sep 17 00:00:00 2001 From: Takagi-Masao <3358487866@qq.com> Date: Wed, 14 Oct 2026 18:29:31 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20ADT=E3=82=BF=E3=83=96=E3=81=AE?= =?UTF-8?q?=E3=83=9A=E3=83=BC=E3=82=B8=E3=83=8D=E3=83=BC=E3=82=B7=E3=83=A7?= =?UTF-8?q?=E3=83=B3=E3=81=A7=E3=83=96=E3=83=A9=E3=82=A6=E3=82=B6=E3=81=AE?= =?UTF-8?q?=E3=83=95=E3=83=AA=E3=83=BC=E3=82=BA=E3=82=92=E9=98=B2=E3=81=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/TablePage/AtCoderRegularTable.tsx | 20 +++ .../src/pages/TablePage/index.test.tsx | 165 ++++++++++++++++++ .../src/pages/TablePage/index.tsx | 2 + 3 files changed, 187 insertions(+) create mode 100644 atcoder-problems-frontend/src/pages/TablePage/index.test.tsx diff --git a/atcoder-problems-frontend/src/pages/TablePage/AtCoderRegularTable.tsx b/atcoder-problems-frontend/src/pages/TablePage/AtCoderRegularTable.tsx index 7b7e0753f..c959f8006 100644 --- a/atcoder-problems-frontend/src/pages/TablePage/AtCoderRegularTable.tsx +++ b/atcoder-problems-frontend/src/pages/TablePage/AtCoderRegularTable.tsx @@ -23,6 +23,10 @@ import { SubmitTimespan } from "../../components/SubmitTimespan"; import { RatingInfo } from "../../utils/RatingInfo"; import { ProblemPoint } from "../../components/Problempoint"; import { classifyContest } from "../../utils/ContestClassifier"; +import { + ListPaginationPanel, + ListPaginationPanelProps, +} from "../../components/ListPaginationPanel"; interface Props { contests: Contest[]; @@ -35,6 +39,7 @@ interface Props { showPenalties: boolean; selectedLanguages: Set; userRatingInfo: RatingInfo; + paginate?: boolean; } const getProblemHeaderAlphabet = (problem: MergedProblem, contest: Contest) => { @@ -130,6 +135,21 @@ const AtCoderRegularTableSFC: React.FC = (props) => {

{props.title}

; + }, + }} tableContainerClass="contest-table-responsive contest-regular-table-responsive" > ({ + id: `adt_all_${index}`, + title: `Daily Training ${index}`, + start_epoch_second: index, + duration_second: 3600, + rate_change: "-", +})); +const mockContestToProblems = new Map( + mockContests.map((contest, index) => [ + contest.id, + [ + { + id: `problem_${index}`, + contest_id: contest.id, + problem_index: "A", + name: `Problem ${index}`, + first_user_id: null, + first_contest_id: null, + first_submission_id: null, + fastest_user_id: null, + fastest_contest_id: null, + fastest_submission_id: null, + execution_time: null, + shortest_user_id: null, + shortest_contest_id: null, + shortest_submission_id: null, + source_code_length: null, + solver_count: null, + }, + ], + ]) +); +mockContests.push({ + id: "abc001", + title: "AtCoder Beginner Contest 001", + start_epoch_second: 0, + duration_second: 3600, + rate_change: "All", +}); +const mockSubmissions: Submission[] = []; +const mockRatingInfo = new RatingInfo(0, 0); + +jest.mock("../../api/APIClient", () => ({ + useContests: () => ({ data: mockContests }), + useContestToMergedProblems: () => mockContestToProblems, + useRatingInfo: () => mockRatingInfo, + useUserSubmission: () => mockSubmissions, + useMultipleUserSubmissions: () => ({ data: mockSubmissions }), + useProblemModelMap: () => new Map(), +})); +jest.mock("../../api/InternalAPIClient", () => ({ + useLoginState: () => ({ data: undefined }), + useProgressResetList: () => ({ data: undefined }), +})); + +describe("ADT table pagination", () => { + let container: HTMLDivElement; + + beforeEach(() => { + localStorage.clear(); + localStorage.setItem("contestTableTab", JSON.stringify("ADT")); + mockSubmissions.length = 0; + container = document.createElement("div"); + document.body.appendChild(container); + }); + + afterEach(() => { + void act(() => { + ReactDOM.unmountComponentAtNode(container); + }); + container.remove(); + }); + + const renderPage = () => { + void act(() => { + ReactDOM.render(, container); + }); + }; + const rows = () => + container.querySelectorAll(".react-bs-container-body tbody tr"); + const click = (selector: string) => { + const target = container.querySelector(selector); + if (!target) { + throw new Error(`Missing element: ${selector}`); + } + void act(() => { + Simulate.click(target); + }); + }; + const switchTab = (title: string) => { + const target = Array.from( + container.querySelectorAll(".table-tab button") + ).find((button) => button.textContent === title); + if (!target) { + throw new Error(`Missing tab: ${title}`); + } + void act(() => { + Simulate.click(target); + }); + }; + + it("limits mounted rows and lets users reach the oldest contests", () => { + renderPage(); + expect(rows()).toHaveLength(50); + expect(rows()[0].textContent).toContain("ADT_ALL_150"); + click('.pagination li[title="2"] .page-link'); + expect(rows()).toHaveLength(50); + expect(rows()[0].textContent).toContain("ADT_ALL_100"); + click('.pagination li[title="4"] .page-link'); + expect(rows()).toHaveLength(1); + expect(rows()[0].textContent).toContain("ADT_ALL_0"); + }); + + it("returns to the newest contests after switching tabs", () => { + renderPage(); + click('.pagination li[title="4"] .page-link'); + switchTab("ABC"); + expect(rows()).toHaveLength(1); + expect(container.querySelector(".pagination")).toBeNull(); + switchTab("ADT"); + expect(rows()).toHaveLength(50); + expect(rows()[0].textContent).toContain("ADT_ALL_150"); + }); + + it("filters all contests before paginating and handles a disappearing last page", () => { + mockSubmissions.push({ + id: 1, + epoch_second: 1, + problem_id: "problem_150", + contest_id: "adt_all_150", + user_id: "user", + language: "C++", + result: "AC", + point: 100, + length: 100, + execution_time: 1, + }); + renderPage(); + expect(rows()[0].querySelector(".table-success")).not.toBeNull(); + click('.pagination li[title="4"] .page-link'); + const filter = container.querySelector("#hideCompletedContest"); + if (!filter) { + throw new Error("Missing completed contest filter"); + } + void act(() => { + Simulate.change(filter); + }); + expect(rows()).toHaveLength(50); + expect(rows()[0].textContent).toContain("ADT_ALL_149"); + expect(container.querySelector('.pagination li[title="4"]')).toBeNull(); + click('.pagination li[title="3"] .page-link'); + expect(rows()).toHaveLength(50); + expect(rows()[49].textContent).toContain("ADT_ALL_0"); + }); +}); diff --git a/atcoder-problems-frontend/src/pages/TablePage/index.tsx b/atcoder-problems-frontend/src/pages/TablePage/index.tsx index 297e37449..588578d82 100644 --- a/atcoder-problems-frontend/src/pages/TablePage/index.tsx +++ b/atcoder-problems-frontend/src/pages/TablePage/index.tsx @@ -141,6 +141,8 @@ export const TablePage: React.FC = (props) => { "PAST", ].includes(activeTab) ? ( Date: Wed, 14 Oct 2026 22:34:20 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E3=82=B3=E3=83=B3=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E8=A1=A8=E3=81=AE=E3=83=9A=E3=83=BC=E3=82=B8=E3=83=8D?= =?UTF-8?q?=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E3=82=92List=E3=83=9A?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=81=AB=E7=B5=B1=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/TablePage/AtCoderRegularTable.tsx | 8 +- .../src/pages/TablePage/index.test.tsx | 113 +++++++++++++----- .../src/pages/TablePage/index.tsx | 1 - 3 files changed, 89 insertions(+), 33 deletions(-) diff --git a/atcoder-problems-frontend/src/pages/TablePage/AtCoderRegularTable.tsx b/atcoder-problems-frontend/src/pages/TablePage/AtCoderRegularTable.tsx index c959f8006..122322757 100644 --- a/atcoder-problems-frontend/src/pages/TablePage/AtCoderRegularTable.tsx +++ b/atcoder-problems-frontend/src/pages/TablePage/AtCoderRegularTable.tsx @@ -39,7 +39,6 @@ interface Props { showPenalties: boolean; selectedLanguages: Set; userRatingInfo: RatingInfo; - paginate?: boolean; } const getProblemHeaderAlphabet = (problem: MergedProblem, contest: Contest) => { @@ -135,14 +134,15 @@ const AtCoderRegularTableSFC: React.FC = (props) => {

{props.title}

({ +const mockRegularCategories = [ + { category: "ABC", prefix: "abc", digits: 3, rateChange: "-" }, + { category: "ARC", prefix: "arc", digits: 3, rateChange: "-" }, + { category: "AGC", prefix: "agc", digits: 3, rateChange: "-" }, + { category: "AWC", prefix: "awc", digits: 4, rateChange: "-" }, + { + category: "ABC-Like", + prefix: "abc_like_", + digits: 3, + rateChange: "~ 1999", + }, + { + category: "ARC-Like", + prefix: "arc_like_", + digits: 3, + rateChange: "~ 2799", + }, + { category: "AGC-Like", prefix: "agc_like_", digits: 3, rateChange: "All" }, + { category: "PAST", prefix: "past", digits: 3, rateChange: "-" }, +]; +const mockContests: Contest[] = Array.from({ length: 161 }, (_, index) => ({ id: `adt_all_${index}`, title: `Daily Training ${index}`, start_epoch_second: index, duration_second: 3600, rate_change: "-", })); +mockContests.push( + ...mockRegularCategories.flatMap(({ category, prefix, digits, rateChange }) => + Array.from({ length: 51 }, (_, index) => ({ + id: prefix + index.toString().padStart(digits, "0"), + title: `${category} ${index}`, + start_epoch_second: 1600000000 + index, + duration_second: 3600, + rate_change: rateChange, + })) + ) +); const mockContestToProblems = new Map( mockContests.map((contest, index) => [ contest.id, @@ -40,13 +71,6 @@ const mockContestToProblems = new Map( ], ]) ); -mockContests.push({ - id: "abc001", - title: "AtCoder Beginner Contest 001", - start_epoch_second: 0, - duration_second: 3600, - rate_change: "All", -}); const mockSubmissions: Submission[] = []; const mockRatingInfo = new RatingInfo(0, 0); @@ -63,7 +87,7 @@ jest.mock("../../api/InternalAPIClient", () => ({ useProgressResetList: () => ({ data: undefined }), })); -describe("ADT table pagination", () => { +describe("Regular contest table pagination", () => { let container: HTMLDivElement; beforeEach(() => { @@ -111,33 +135,36 @@ describe("ADT table pagination", () => { it("limits mounted rows and lets users reach the oldest contests", () => { renderPage(); - expect(rows()).toHaveLength(50); - expect(rows()[0].textContent).toContain("ADT_ALL_150"); + expect(rows()).toHaveLength(20); + expect(rows()[0].textContent).toContain("ADT_ALL_160"); click('.pagination li[title="2"] .page-link'); - expect(rows()).toHaveLength(50); - expect(rows()[0].textContent).toContain("ADT_ALL_100"); - click('.pagination li[title="4"] .page-link'); + expect(rows()).toHaveLength(20); + expect(rows()[0].textContent).toContain("ADT_ALL_140"); + click('.pagination li[title="9"] .page-link'); expect(rows()).toHaveLength(1); expect(rows()[0].textContent).toContain("ADT_ALL_0"); }); it("returns to the newest contests after switching tabs", () => { renderPage(); - click('.pagination li[title="4"] .page-link'); + click('.pagination li[title="9"] .page-link'); switchTab("ABC"); - expect(rows()).toHaveLength(1); - expect(container.querySelector(".pagination")).toBeNull(); + expect(rows()).toHaveLength(20); + expect(rows()[0].textContent).toContain("ABC050"); + click('.pagination li[title="2"] .page-link'); switchTab("ADT"); - expect(rows()).toHaveLength(50); - expect(rows()[0].textContent).toContain("ADT_ALL_150"); + expect(rows()).toHaveLength(20); + expect(rows()[0].textContent).toContain("ADT_ALL_160"); + switchTab("ABC"); + expect(rows()[0].textContent).toContain("ABC050"); }); it("filters all contests before paginating and handles a disappearing last page", () => { mockSubmissions.push({ id: 1, epoch_second: 1, - problem_id: "problem_150", - contest_id: "adt_all_150", + problem_id: "problem_160", + contest_id: "adt_all_160", user_id: "user", language: "C++", result: "AC", @@ -147,7 +174,7 @@ describe("ADT table pagination", () => { }); renderPage(); expect(rows()[0].querySelector(".table-success")).not.toBeNull(); - click('.pagination li[title="4"] .page-link'); + click('.pagination li[title="9"] .page-link'); const filter = container.querySelector("#hideCompletedContest"); if (!filter) { throw new Error("Missing completed contest filter"); @@ -155,11 +182,41 @@ describe("ADT table pagination", () => { void act(() => { Simulate.change(filter); }); - expect(rows()).toHaveLength(50); - expect(rows()[0].textContent).toContain("ADT_ALL_149"); - expect(container.querySelector('.pagination li[title="4"]')).toBeNull(); - click('.pagination li[title="3"] .page-link'); - expect(rows()).toHaveLength(50); - expect(rows()[49].textContent).toContain("ADT_ALL_0"); + expect(rows()).toHaveLength(20); + expect(rows()[0].textContent).toContain("ADT_ALL_159"); + expect(container.querySelector('.pagination li[title="9"]')).toBeNull(); + click('.pagination li[title="8"] .page-link'); + expect(rows()).toHaveLength(20); + expect(rows()[19].textContent).toContain("ADT_ALL_0"); }); + + it.each(["ADT", ...mockRegularCategories.map(({ category }) => category)])( + "paginates %s contests with the shared List page controls", + (category) => { + localStorage.setItem("contestTableTab", JSON.stringify(category)); + renderPage(); + expect(rows()).toHaveLength(20); + const firstContest = rows()[0].textContent; + click('.pagination li[title="2"] .page-link'); + expect(rows()).toHaveLength(20); + expect(rows()[0].textContent).not.toBe(firstContest); + click('.pagination li[title="1"] .page-link'); + click(".react-bs-table-sizePerPage-dropdown .dropdown-toggle"); + const sizes = Array.from( + container.querySelectorAll( + ".react-bs-table-sizePerPage-dropdown .dropdown-item" + ) + ); + expect(sizes.map((item) => item.textContent)).toEqual([ + "20", + "50", + "100", + "200", + ]); + void act(() => { + Simulate.click(sizes[1]); + }); + expect(rows()).toHaveLength(50); + } + ); }); diff --git a/atcoder-problems-frontend/src/pages/TablePage/index.tsx b/atcoder-problems-frontend/src/pages/TablePage/index.tsx index 588578d82..9bae282a7 100644 --- a/atcoder-problems-frontend/src/pages/TablePage/index.tsx +++ b/atcoder-problems-frontend/src/pages/TablePage/index.tsx @@ -142,7 +142,6 @@ export const TablePage: React.FC = (props) => { ].includes(activeTab) ? (