fix: validate per-step fields before advancing Create Server wizard
- Add STEP_FIELDS constant mapping each step to its required fields - Extract trigger() from useForm and call it on Next click - Only advance to next step when trigger() returns true, blocking silent failures where invalid data could reach the Review step - Add CreateServerPage.test.tsx with 8 tests covering step navigation, validation blocking, happy path, and submit mutation - Update CLAUDE.md: mark /servers/new Complete, remove resolved bug - Mark implementation plan as completed
This commit is contained in:
197
frontend/src/__tests__/CreateServerPage.test.tsx
Normal file
197
frontend/src/__tests__/CreateServerPage.test.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { MemoryRouter, Route, Routes } from "react-router-dom";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
import { CreateServerPage } from "@/pages/CreateServerPage";
|
||||
import { useCreateServer } from "@/hooks/useServers";
|
||||
import { useGamesList } from "@/hooks/useGames";
|
||||
import { useAuthStore } from "@/store/auth.store";
|
||||
import { useUIStore } from "@/store/ui.store";
|
||||
|
||||
vi.mock("@/hooks/useServers", () => ({
|
||||
useCreateServer: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/useGames", () => ({
|
||||
useGamesList: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/store/auth.store", () => ({
|
||||
useAuthStore: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/store/ui.store", () => ({
|
||||
useUIStore: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockMutateAsync = vi.fn();
|
||||
const mockAddNotification = vi.fn();
|
||||
|
||||
function renderPage() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
|
||||
return {
|
||||
user: userEvent.setup(),
|
||||
...render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter initialEntries={["/servers/new"]}>
|
||||
<Routes>
|
||||
<Route path="/servers/new" element={<CreateServerPage />} />
|
||||
<Route path="/" element={<div>Dashboard</div>} />
|
||||
<Route path="/servers/:id" element={<div>Server Detail</div>} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
describe("CreateServerPage", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(useAuthStore).mockReturnValue("admin");
|
||||
vi.mocked(useUIStore).mockReturnValue(mockAddNotification);
|
||||
vi.mocked(useGamesList).mockReturnValue({ data: undefined } as ReturnType<typeof useGamesList>);
|
||||
vi.mocked(useCreateServer).mockReturnValue({
|
||||
mutateAsync: mockMutateAsync,
|
||||
isPending: false,
|
||||
} as unknown as ReturnType<typeof useCreateServer>);
|
||||
mockMutateAsync.mockReset();
|
||||
mockAddNotification.mockReset();
|
||||
});
|
||||
|
||||
it("renders step 0 (Game Type) by default", () => {
|
||||
renderPage();
|
||||
expect(screen.getByText("Create Server")).toBeInTheDocument();
|
||||
expect(screen.getByRole("combobox")).toBeInTheDocument();
|
||||
expect(screen.queryByPlaceholderText("My Arma Server")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Next on step 0 advances to step 1", async () => {
|
||||
const { user } = renderPage();
|
||||
await user.click(screen.getByRole("button", { name: /next/i }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText("My Arma Server")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("Next on step 1 with empty name blocks advance and shows error", async () => {
|
||||
const { user } = renderPage();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /next/i }));
|
||||
await waitFor(() => expect(screen.getByPlaceholderText("My Arma Server")).toBeInTheDocument());
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /next/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Server name is required")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByPlaceholderText("My Arma Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Next on step 1 with empty exe_path blocks advance and shows error", async () => {
|
||||
const { user } = renderPage();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /next/i }));
|
||||
await waitFor(() => expect(screen.getByPlaceholderText("My Arma Server")).toBeInTheDocument());
|
||||
|
||||
await user.type(screen.getByPlaceholderText("My Arma Server"), "Test Server");
|
||||
await user.click(screen.getByRole("button", { name: /next/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Executable path is required")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByPlaceholderText("My Arma Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Next on step 1 with valid data advances to step 2", async () => {
|
||||
const { user } = renderPage();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /next/i }));
|
||||
await waitFor(() => expect(screen.getByPlaceholderText("My Arma Server")).toBeInTheDocument());
|
||||
|
||||
await user.type(screen.getByPlaceholderText("My Arma Server"), "My Server");
|
||||
await user.type(
|
||||
screen.getByPlaceholderText("D:/Arma3Server/arma3server_x64.exe"),
|
||||
"C:/server/arma3.exe",
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /next/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/auto-restart on crash/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("Back button on step 1 returns to step 0", async () => {
|
||||
const { user } = renderPage();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /next/i }));
|
||||
await waitFor(() => expect(screen.getByPlaceholderText("My Arma Server")).toBeInTheDocument());
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /back/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("combobox")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByPlaceholderText("My Arma Server")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("full happy path reaches Review step", async () => {
|
||||
const { user } = renderPage();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /next/i }));
|
||||
await waitFor(() => expect(screen.getByPlaceholderText("My Arma Server")).toBeInTheDocument());
|
||||
|
||||
await user.type(screen.getByPlaceholderText("My Arma Server"), "My Server");
|
||||
await user.type(
|
||||
screen.getByPlaceholderText("D:/Arma3Server/arma3server_x64.exe"),
|
||||
"C:/server/arma3.exe",
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /next/i }));
|
||||
await waitFor(() => expect(screen.getByLabelText(/auto-restart on crash/i)).toBeInTheDocument());
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /next/i }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Review Configuration")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByRole("button", { name: /create server/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("submit fires createServer mutation on valid data", async () => {
|
||||
mockMutateAsync.mockResolvedValueOnce({ data: { id: 42 } });
|
||||
|
||||
const { user } = renderPage();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /next/i }));
|
||||
await waitFor(() => expect(screen.getByPlaceholderText("My Arma Server")).toBeInTheDocument());
|
||||
|
||||
await user.type(screen.getByPlaceholderText("My Arma Server"), "My Server");
|
||||
await user.type(
|
||||
screen.getByPlaceholderText("D:/Arma3Server/arma3server_x64.exe"),
|
||||
"C:/server/arma3.exe",
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /next/i }));
|
||||
await waitFor(() => expect(screen.getByLabelText(/auto-restart on crash/i)).toBeInTheDocument());
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /next/i }));
|
||||
await waitFor(() => expect(screen.getByText("Review Configuration")).toBeInTheDocument());
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /create server/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockMutateAsync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: "My Server",
|
||||
exe_path: "C:/server/arma3.exe",
|
||||
game_type: "arma3",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user