fix(panel): 服务器列表的刷新按钮只为自己的点击转圈和禁用,后台轮询时保持可点
This commit is contained in:
2 files changed
+51
-3
No files matched your search
@@ -428,3 +428,41 @@ describe("ServersPage with nothing matching", () => {
|
|||||||
expect(await tableOrder()).toEqual(names.slice(0, 20));
|
expect(await tableOrder()).toEqual(names.slice(0, 20));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("ServersPage refresh button", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stays still and clickable through the background reread, and spins for its own click", async () => {
|
||||||
|
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||||
|
calls.fleet.mockResolvedValue([row("survival", {})]);
|
||||||
|
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<ServersPage />
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
await tableOrder();
|
||||||
|
const button = screen.getByRole("button", { name: "Refresh now" }) as HTMLButtonElement;
|
||||||
|
const spinning = () => button.querySelector("svg")?.classList.contains("animate-spin");
|
||||||
|
|
||||||
|
// The background reread hangs; the button does not show it.
|
||||||
|
let finish: (v: FleetServer[]) => void = () => {};
|
||||||
|
calls.fleet.mockReturnValueOnce(new Promise<FleetServer[]>((r) => (finish = r)));
|
||||||
|
await act(() => vi.advanceTimersByTimeAsync(10_000));
|
||||||
|
expect(calls.fleet).toHaveBeenCalledTimes(2);
|
||||||
|
expect(button.disabled).toBe(false);
|
||||||
|
expect(spinning()).toBe(false);
|
||||||
|
|
||||||
|
calls.fleet.mockReturnValueOnce(new Promise<FleetServer[]>((r) => (finish = r)));
|
||||||
|
await user.click(button);
|
||||||
|
expect(calls.fleet).toHaveBeenCalledTimes(3);
|
||||||
|
expect(button.disabled).toBe(true);
|
||||||
|
expect(spinning()).toBe(true);
|
||||||
|
|
||||||
|
await act(async () => finish([row("survival", {})]));
|
||||||
|
expect(button.disabled).toBe(false);
|
||||||
|
expect(spinning()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -148,6 +148,16 @@ export function ServersPage() {
|
|||||||
|
|
||||||
usePolling(reload, REFRESH_MS);
|
usePolling(reload, REFRESH_MS);
|
||||||
|
|
||||||
|
// The refresh button answers its own click alone. The background reread every
|
||||||
|
// few seconds leaves it still and clickable; a spin and a disabled button on
|
||||||
|
// each tick read as the page stalling.
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
if (refreshing && !loading) setRefreshing(false);
|
||||||
|
const refresh = () => {
|
||||||
|
setRefreshing(true);
|
||||||
|
reload();
|
||||||
|
};
|
||||||
|
|
||||||
const servers = useMemo<UnifiedServer[]>(() => {
|
const servers = useMemo<UnifiedServer[]>(() => {
|
||||||
if (!data) return [];
|
if (!data) return [];
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
@@ -253,12 +263,12 @@ export function ServersPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={reload}
|
onClick={refresh}
|
||||||
disabled={loading}
|
disabled={refreshing}
|
||||||
aria-label={t("fleet_refresh")}
|
aria-label={t("fleet_refresh")}
|
||||||
title={t("fleet_refresh")}
|
title={t("fleet_refresh")}
|
||||||
>
|
>
|
||||||
<RefreshCw className={cn("h-4 w-4", loading && "animate-spin")} />
|
<RefreshCw className={cn("h-4 w-4", refreshing && "animate-spin")} />
|
||||||
</Button>
|
</Button>
|
||||||
{isAdmin && cfg && <CreateServerDialog cfg={cfg} onCreated={reload} />}
|
{isAdmin && cfg && <CreateServerDialog cfg={cfg} onCreated={reload} />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user