import { afterEach, describe, expect, it, vi } from "vitest"; import { cleanup, fireEvent, render, screen, waitFor, within, } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { Refine } from "@refinedev/core"; import { MemoryRouter } from "react-router"; import { NetworkExitList } from "./NetworkExitsPage"; afterEach(() => { cleanup(); vi.restoreAllMocks(); }); const httpError = (message, status, body) => Object.assign(new Error(message), { status, body }); const networkExit = { id: "exit-a", protocol: "socks5", host: "proxy.example", port: 1080, username: "proxy-user", password: "plain-password", health_status: "healthy", observed_public_ip: "203.0.113.1", }; function provider(exits = [], overrides = {}) { return { getList: vi.fn(({ resource }) => Promise.resolve( resource === "network-exits" ? { data: exits, total: exits.length } : { data: [], total: 0 }, ), ), create: vi.fn().mockResolvedValue({ data: networkExit }), networkExitAction: vi.fn().mockResolvedValue(networkExit), getOne: vi.fn(), getMany: vi.fn(), getManyReference: vi.fn(), update: vi.fn(), updateMany: vi.fn(), delete: vi.fn(), deleteMany: vi.fn(), ...overrides, }; } const renderExits = (dataProvider) => render( , ); describe("NetworkExitList", () => { it("shows the ordered empty-state instruction", async () => { renderExits(provider()); expect(await screen.findByText(/创建并检测健康后/)).toBeTruthy(); }); it("shows the exit ID as the primary identifier with stored credentials", async () => { const dataProvider = provider([networkExit]); renderExits(dataProvider); const idLink = await screen.findByRole("link", { name: "exit-a" }); expect(idLink.getAttribute("href")).toBe("/network-exits/exit-a"); expect(screen.getByText("socks5://proxy.example:1080")).toBeTruthy(); expect((await screen.findAllByText(/proxy-user/)).length).toBeGreaterThan( 0, ); expect( (await screen.findAllByText(/plain-password/)).length, ).toBeGreaterThan(0); fireEvent.click(screen.getAllByRole("button", { name: "检测" })[0]); await waitFor(() => expect(dataProvider.networkExitAction).toHaveBeenCalledWith( "exit-a", "check", ), ); }); it("creates with plain credentials and closes on success", async () => { const dataProvider = provider(); renderExits(dataProvider); fireEvent.click( await screen.findByRole("button", { name: "创建网络出口" }), ); const dialog = screen.getByRole("dialog"); fireEvent.change(within(dialog).getByRole("textbox", { name: "主机" }), { target: { value: "proxy.example" }, }); fireEvent.change(within(dialog).getByRole("spinbutton", { name: "端口" }), { target: { value: "1080" }, }); fireEvent.change( within(dialog).getByRole("textbox", { name: "用户名(可选)" }), { target: { value: "proxy-user" } }, ); fireEvent.change( within(dialog).getByRole("textbox", { name: "密码(可选)" }), { target: { value: "plain-password" } }, ); fireEvent.click( within(dialog).getByRole("button", { name: "创建网络出口" }), ); await waitFor(() => expect(dataProvider.create).toHaveBeenCalledWith({ resource: "network-exits", variables: { protocol: "socks5", host: "proxy.example", port: 1080, username: "proxy-user", password: "plain-password", expected_public_ip: "", expected_region: "", }, }), ); await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull()); }); it("keeps the create modal open after a failure", async () => { const dataProvider = provider([], { create: vi.fn().mockRejectedValue(new Error("创建失败")), }); renderExits(dataProvider); fireEvent.click( await screen.findByRole("button", { name: "创建网络出口" }), ); const dialog = screen.getByRole("dialog"); fireEvent.change(within(dialog).getByRole("textbox", { name: "主机" }), { target: { value: "proxy.example" }, }); fireEvent.change(within(dialog).getByRole("spinbutton", { name: "端口" }), { target: { value: "1080" }, }); fireEvent.click( within(dialog).getByRole("button", { name: "创建网络出口" }), ); expect((await within(dialog).findByRole("alert")).textContent).toContain( "创建失败", ); expect(screen.getByRole("dialog")).toBeTruthy(); }); it("shows unknown bindings and retries when browsers return 502", async () => { const dataProvider = provider([networkExit], { getList: vi.fn(({ resource }) => resource === "browsers" ? Promise.reject(httpError("bad gateway", 502)) : Promise.resolve({ data: [networkExit], total: 1 }), ), }); renderExits(dataProvider); expect(await screen.findByText(/环境不可用/)).toBeTruthy(); expect( (await screen.findAllByText(/绑定账号:状态未知|状态未知/)).length, ).toBeGreaterThan(0); expect(screen.queryByText(/绑定账号:未绑定/)).toBeNull(); expect(screen.queryByText(/^未绑定$/)).toBeNull(); expect(screen.getAllByRole("button", { name: "检测" })[0].disabled).toBe( false, ); fireEvent.click(screen.getByRole("button", { name: "重试环境状态" })); await waitFor(() => expect( dataProvider.getList.mock.calls.filter( ([call]) => call.resource === "browsers", ).length, ).toBeGreaterThan(1), ); }); });