Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions webapp/src/api.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { vi, describe, it, expect, beforeEach, afterEach } from "vitest";
import api, { makeRequest, setSessionIdForApi, onSessionExpired } from "./api";

// A custom adapter captures the outgoing config without hitting the network.
// The response interceptor still runs on rejected responses.
const captureAdapter = () => {
const captured = {};
const adapter = (config) => {
captured.config = config;
return Promise.reject(new Error("no network"));
};
return { captured, adapter };
};

// Axios stringifies JSON bodies before the adapter runs.
const body = (config) => JSON.parse(config.data);

describe("api request interceptor", () => {
beforeEach(() => {
setSessionIdForApi(null);
});
afterEach(() => {
delete api.defaults.adapter;
});

it("makeRequest adds session_id when sessionId is provided", async () => {
const { captured, adapter } = captureAdapter();
api.defaults.adapter = adapter;
await makeRequest("/x", { a: 1 }, "sess-1").catch(() => {});
expect(captured.config.url).toBe("/x");
expect(body(captured.config)).toEqual({ a: 1, session_id: "sess-1" });
});

it("makeRequest does not add session_id when omitted", async () => {
const { captured, adapter } = captureAdapter();
api.defaults.adapter = adapter;
await makeRequest("/x", { a: 1 }).catch(() => {});
expect(body(captured.config)).toEqual({ a: 1 });
});

it("injects active session id into post bodies", async () => {
const { captured, adapter } = captureAdapter();
api.defaults.adapter = adapter;
setSessionIdForApi("active-id");
await api.post("/y", { b: 2 }).catch(() => {});
expect(body(captured.config)).toEqual({ b: 2, session_id: "active-id" });
});

it("does not override an existing session_id", async () => {
const { captured, adapter } = captureAdapter();
api.defaults.adapter = adapter;
setSessionIdForApi("active-id");
await api.post("/y", { session_id: "already-set" }).catch(() => {});
expect(body(captured.config).session_id).toBe("already-set");
});

it("leaves non-POST requests untouched", async () => {
const { captured, adapter } = captureAdapter();
api.defaults.adapter = adapter;
setSessionIdForApi("active-id");
await api.get("/y").catch(() => {});
expect(captured.config.data).toBeUndefined();
});

it("creates a data object when a POST body is missing", async () => {
const { captured, adapter } = captureAdapter();
api.defaults.adapter = adapter;
setSessionIdForApi("active-id");
await api.post("/y").catch(() => {});
expect(body(captured.config)).toEqual({ session_id: "active-id" });
});
});

describe("api response interceptor", () => {
it("triggers session expiry callback on 404", async () => {
const cb = vi.fn();
onSessionExpired(cb);
const adapter404 = () => Promise.reject({ response: { status: 404 } });
const err = await api
.post("/gone", {}, { adapter: adapter404 })
.catch((e) => e);
expect(err.response.status).toBe(404);
expect(cb).toHaveBeenCalledTimes(1);
onSessionExpired(null);
});

it("does not trigger callback on other statuses", async () => {
const cb = vi.fn();
onSessionExpired(cb);
const adapter500 = () => Promise.reject({ response: { status: 500 } });
const err = await api
.post("/boom", {}, { adapter: adapter500 })
.catch((e) => e);
expect(err.response.status).toBe(500);
expect(cb).not.toHaveBeenCalled();
onSessionExpired(null);
});
});
124 changes: 100 additions & 24 deletions webapp/src/components/Breakpoint/__tests__/Breakpoint.test.jsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,116 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import Breakpoint from "../Breakpoint.jsx";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { vi } from "vitest";

vi.mock("../../../context/DataContext.jsx", () => ({
DataState: () => ({
sessionId: 'test-session-123',
const state = vi.hoisted(() => ({
value: {
sessionId: "test-session-123",
sessionLoading: false,
sessionError: null,
createSession: vi.fn(),
clearSessionError: vi.fn(),
}),
},
}));

// Breakpoint
test("renders Breakpoint component with basic structure", () => {
render(<Breakpoint />);
vi.mock("../../../context/DataContext.jsx", () => ({
DataState: () => state.value,
}));
vi.mock("react-toastify", () => ({
ToastContainer: () => null,
toast: { error: vi.fn(), success: vi.fn() },
}));
vi.mock("../../../api", () => ({
makeRequest: vi.fn(),
}));

const addBreakpointElement = screen.getByText(/Add Breakpoint/i);
expect(addBreakpointElement).toBeInTheDocument();
import { toast } from "react-toastify";
import { makeRequest } from "../../../api";
import Breakpoint from "../Breakpoint.jsx";

const lineInputs = screen.getAllByRole("textbox");
expect(lineInputs).toHaveLength(2);
});
describe("Breakpoint", () => {
beforeEach(() => {
state.value = {
sessionId: "test-session-123",
sessionLoading: false,
sessionError: null,
createSession: vi.fn(),
clearSessionError: vi.fn(),
};
makeRequest.mockReset();
toast.error.mockClear();
toast.success.mockClear();
});

test("renders Breakpoint component and checks about text Line", async () => {
render(<Breakpoint />);
const lineLabel = screen.getByText(/Line/i);
expect(lineLabel).toBeInTheDocument();
});
test("renders the add breakpoint form", () => {
render(<Breakpoint />);
expect(screen.getByText("Add Breakpoint")).toBeInTheDocument();
expect(screen.getAllByRole("textbox")).toHaveLength(2);
});

test("shows loading state while initializing", () => {
state.value.sessionLoading = true;
render(<Breakpoint />);
expect(screen.getByText("Initializing debug session...")).toBeInTheDocument();
});

test("shows error banner and starts a new session", () => {
state.value.sessionError = "Session failed";
render(<Breakpoint />);
expect(screen.getByText("Session failed")).toBeInTheDocument();
fireEvent.click(screen.getByText("Start New Session"));
expect(state.value.clearSessionError).toHaveBeenCalled();
expect(state.value.createSession).toHaveBeenCalled();
});

test("shows no-session state and starts a debug session", () => {
state.value.sessionId = null;
render(<Breakpoint />);
expect(screen.getByText("No active session.")).toBeInTheDocument();
fireEvent.click(screen.getByText("Start Debug Session"));
expect(state.value.createSession).toHaveBeenCalled();
});

test("toasts an error when adding a breakpoint with no fields", () => {
render(<Breakpoint />);
fireEvent.click(screen.getByText("Add"));
expect(toast.error).toHaveBeenCalledWith("Enter any of the field", {
autoClose: 1000,
});
expect(makeRequest).not.toHaveBeenCalled();
});

test("saves a breakpoint by line and toasts success", async () => {
makeRequest.mockResolvedValue({ data: { success: true } });
render(<Breakpoint />);

const inputs = screen.getAllByRole("textbox");
fireEvent.change(inputs[0], { target: { value: "10" } });
fireEvent.click(screen.getByText("Add"));

expect(makeRequest).toHaveBeenCalledWith(
"/set_breakpoint",
{ location: "10", name: "program" },
"test-session-123"
);
await waitFor(() =>
expect(toast.success).toHaveBeenCalledWith("Added breakpoint", {
autoClose: 1000,
})
);
});

test("typing in Line input updates the value correctly", () => {
render(<Breakpoint />);
test("toasts an error when the breakpoint request fails", async () => {
makeRequest.mockRejectedValue(new Error("boom"));
render(<Breakpoint />);

const lineInput = screen.getAllByRole("textbox");
fireEvent.change(lineInput[0], { target: { value: "123" } });
const inputs = screen.getAllByRole("textbox");
fireEvent.change(inputs[1], { target: { value: "main" } });
fireEvent.click(screen.getByText("Add"));

expect(lineInput[0]).toHaveValue("123");
await waitFor(() =>
expect(toast.error).toHaveBeenCalledWith("Something went Wrong", {
autoClose: 1000,
})
);
});
});
102 changes: 90 additions & 12 deletions webapp/src/components/Functions/__tests__/Functions.test.jsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,105 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import Functions from "../Functions.jsx";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { vi } from "vitest";

vi.mock("../../../context/DataContext.jsx", () => ({
DataState: () => ({
const state = vi.hoisted(() => ({
value: {
refresh: false,
functions: [],
setFunctions: vi.fn(),
sessionId: 'test-session-123',
sessionId: "test-session-123",
sessionLoading: false,
sessionError: null,
createSession: vi.fn(),
clearSessionError: vi.fn(),
}),
},
}));

vi.mock("../../../context/DataContext.jsx", () => ({
DataState: () => state.value,
}));
vi.mock("../../../api", () => ({
makeRequest: vi.fn(),
}));

test("renders Functions component with correct heading", () => {
render(<Functions />);
import { makeRequest } from "../../../api";
import Functions from "../Functions.jsx";

describe("Functions", () => {
beforeEach(() => {
state.value = {
refresh: false,
functions: [],
setFunctions: vi.fn(),
sessionId: "test-session-123",
sessionLoading: false,
sessionError: null,
createSession: vi.fn(),
clearSessionError: vi.fn(),
};
makeRequest.mockReset();
});

test("renders Functions heading with active session", () => {
render(<Functions />);
expect(screen.getByText(/Functions/i)).toBeInTheDocument();
});

test("shows loading state while initializing", () => {
state.value.sessionLoading = true;
render(<Functions />);
expect(screen.getByText("Initializing debug session...")).toBeInTheDocument();
});

test("shows error banner and starts a new session", () => {
state.value.sessionError = "Session failed";
render(<Functions />);
expect(screen.getByText("Session failed")).toBeInTheDocument();
fireEvent.click(screen.getByText("Start New Session"));
expect(state.value.clearSessionError).toHaveBeenCalled();
expect(state.value.createSession).toHaveBeenCalled();
});

test("shows no-session state and starts a debug session", () => {
state.value.sessionId = null;
render(<Functions />);
expect(screen.getByText("No active session.")).toBeInTheDocument();
fireEvent.click(screen.getByText("Start Debug Session"));
expect(state.value.createSession).toHaveBeenCalled();
});

test("fetches functions when refresh fires with a session", async () => {
state.value.refresh = true;
state.value.setFunctions = vi.fn((v) => {
state.value.functions = v;
});
makeRequest.mockResolvedValue({ data: { result: "fn-data" } });

render(<Functions />);

await waitFor(() =>
expect(makeRequest).toHaveBeenCalledWith(
"/get_locals",
{ name: "program" },
"test-session-123"
)
);
await waitFor(() =>
expect(state.value.setFunctions).toHaveBeenCalledWith("fn-data")
);
});

const headingElement = screen.getByText(/Functions/i);
expect(headingElement).toBeInTheDocument();
test("does not fetch when there is no session", async () => {
state.value.refresh = true;
state.value.sessionId = null;
render(<Functions />);
await waitFor(() => expect(makeRequest).not.toHaveBeenCalled());
});

const functionElements = screen.queryAllByRole("link");
expect(functionElements).toHaveLength(0);
test("swallows fetch errors", async () => {
state.value.refresh = true;
makeRequest.mockRejectedValue(new Error("boom"));
render(<Functions />);
await waitFor(() => expect(makeRequest).toHaveBeenCalled());
});
});
Loading
Loading