fix(files): 文件页停服改用控制台同一个按钮,有玩家在线或人数读不到时先确认

This commit is contained in:
Lemon-miaow committed 2026-09-27 06:02:13 +08:00
1 parent 9a238780e5
commit 75dd639cca
6 files changed
+135 -40

No files matched your search

+63 -10
View File
@@ -1,13 +1,13 @@
// @vitest-environment jsdom
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { act, render, screen, within } from "@testing-library/react";
import { act, render, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MemoryRouter, Route, Routes } from "react-router-dom";
import i18next from "i18next";
import { ServerFiles } from "./ServerFiles";
import { STATUS_POLL_FAST_MS } from "@/lib/hooks";
const mocks = vi.hoisted(() => ({ writeServerFile: vi.fn(), status: vi.fn() }));
const mocks = vi.hoisted(() => ({ writeServerFile: vi.fn(), status: vi.fn(), stop: vi.fn() }));
vi.mock("@/lib/api", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/api")>();
@@ -16,6 +16,7 @@ vi.mock("@/lib/api", async (importOriginal) => {
api: {
...actual.api,
status: mocks.status,
stop: mocks.stop,
listServerFiles: () =>
Promise.resolve({
path: "",
@@ -31,7 +32,7 @@ vi.mock("@/lib/tier", () => ({ useTier: () => ({ isAdmin: true, loading: false }
const t = (key: string) => i18next.t(key);
async function openEditor() {
function renderFiles() {
render(
<MemoryRouter initialEntries={["/servers/lobby/files"]}>
<Routes>
@@ -39,6 +40,10 @@ async function openEditor() {
</Routes>
</MemoryRouter>,
);
}
async function openEditor() {
renderFiles();
await userEvent.click(await screen.findByText("server.properties"));
const dialog = await screen.findByRole("dialog");
return { dialog, editor: within(dialog).getByRole("textbox") as HTMLTextAreaElement };
@@ -48,6 +53,7 @@ const stopped = { name: "lobby", subdomain: "lobby", phase: "Stopped", desiredSt
beforeEach(() => {
mocks.writeServerFile.mockReset();
mocks.stop.mockReset();
mocks.status.mockReset();
mocks.status.mockResolvedValue(stopped);
});
@@ -106,13 +112,7 @@ describe("ServerFiles on a running server", () => {
it("lists the files once the server has stopped, without a reload", async () => {
vi.useFakeTimers({ shouldAdvanceTime: true });
mocks.status.mockResolvedValue({ ...stopped, phase: "Running", desiredState: "Running", ready: true });
render(
<MemoryRouter initialEntries={["/servers/lobby/files"]}>
<Routes>
<Route path="/servers/:name/files" element={<ServerFiles />} />
</Routes>
</MemoryRouter>,
);
renderFiles();
expect(await screen.findByText(t("files:stopped_required_title"))).toBeTruthy();
expect(screen.queryByText("server.properties")).toBeNull();
@@ -126,4 +126,57 @@ describe("ServerFiles on a running server", () => {
await act(() => vi.advanceTimersByTimeAsync(STATUS_POLL_FAST_MS * 3));
expect(mocks.status.mock.calls.length).toBe(reads);
});
it("asks before stopping a server with players on it", async () => {
mocks.stop.mockResolvedValue(undefined);
mocks.status.mockResolvedValue({ ...stopped, phase: "Running", desiredState: "Running", ready: true, playersOnline: 2 });
renderFiles();
await userEvent.click(await screen.findByRole("button", { name: t("servers:stop") }));
expect(mocks.stop).not.toHaveBeenCalled();
expect(screen.getByText(i18next.t("servers:stop_confirm_players", { count: 2 }))).toBeTruthy();
const reads = mocks.status.mock.calls.length;
await userEvent.click(screen.getByRole("button", { name: t("servers:stop") }));
expect(mocks.stop).toHaveBeenCalledWith("lobby");
// The page rereads at once to follow the stop.
await waitFor(() => expect(mocks.status.mock.calls.length).toBeGreaterThan(reads));
});
it("asks when the player count cannot be read", async () => {
mocks.status.mockResolvedValue({
...stopped,
phase: "Running",
desiredState: "Running",
ready: true,
playersOnline: 0,
playerCountUnknown: true,
});
renderFiles();
await userEvent.click(await screen.findByRole("button", { name: t("servers:stop") }));
expect(mocks.stop).not.toHaveBeenCalled();
expect(screen.getByText(t("servers:stop_confirm_unknown"))).toBeTruthy();
});
it("offers a failed start a stop, sent without asking, and nothing that would start it", async () => {
mocks.stop.mockResolvedValue(undefined);
// Nobody is on a server that never came up, so even an unreadable count does not ask.
mocks.status.mockResolvedValue({
...stopped,
phase: "Failed",
desiredState: "Running",
ready: false,
playersOnline: 0,
playerCountUnknown: true,
});
renderFiles();
await screen.findByText(t("files:stopped_required_title"));
expect(screen.queryByRole("button", { name: t("servers:retry_start") })).toBeNull();
await userEvent.click(screen.getByRole("button", { name: t("servers:stop") }));
expect(mocks.stop).toHaveBeenCalledWith("lobby");
});
});