Merge branch 'main' into fix/validate-json-on-tool-execution
This commit is contained in:
commit
119922b8b5
5 changed files with 6549 additions and 1285 deletions
|
|
@ -25,6 +25,7 @@ const mockOAuthMetadata = {
|
|||
token_endpoint: "https://oauth.example.com/token",
|
||||
response_types_supported: ["code"],
|
||||
grant_types_supported: ["authorization_code"],
|
||||
scopes_supported: ["read", "write"],
|
||||
};
|
||||
|
||||
const mockOAuthClientInfo = {
|
||||
|
|
@ -36,7 +37,7 @@ const mockOAuthClientInfo = {
|
|||
// Mock MCP SDK functions - must be before imports
|
||||
jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
|
||||
auth: jest.fn(),
|
||||
discoverOAuthMetadata: jest.fn(),
|
||||
discoverAuthorizationServerMetadata: jest.fn(),
|
||||
registerClient: jest.fn(),
|
||||
startAuthorization: jest.fn(),
|
||||
exchangeAuthorization: jest.fn(),
|
||||
|
|
@ -46,7 +47,7 @@ jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
|
|||
|
||||
// Import the functions to get their types
|
||||
import {
|
||||
discoverOAuthMetadata,
|
||||
discoverAuthorizationServerMetadata,
|
||||
registerClient,
|
||||
startAuthorization,
|
||||
exchangeAuthorization,
|
||||
|
|
@ -56,10 +57,62 @@ import {
|
|||
import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
|
||||
import { EMPTY_DEBUGGER_STATE } from "@/lib/auth-types";
|
||||
|
||||
// Mock local auth module
|
||||
jest.mock("@/lib/auth", () => ({
|
||||
DebugInspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({
|
||||
tokens: jest.fn().mockImplementation(() => Promise.resolve(undefined)),
|
||||
clear: jest.fn().mockImplementation(() => {
|
||||
// Mock the real clear() behavior which removes items from sessionStorage
|
||||
sessionStorage.removeItem("[https://example.com/mcp] mcp_tokens");
|
||||
sessionStorage.removeItem("[https://example.com/mcp] mcp_client_info");
|
||||
sessionStorage.removeItem(
|
||||
"[https://example.com/mcp] mcp_server_metadata",
|
||||
);
|
||||
}),
|
||||
redirectUrl: "http://localhost:3000/oauth/callback/debug",
|
||||
clientMetadata: {
|
||||
redirect_uris: ["http://localhost:3000/oauth/callback/debug"],
|
||||
token_endpoint_auth_method: "none",
|
||||
grant_types: ["authorization_code", "refresh_token"],
|
||||
response_types: ["code"],
|
||||
client_name: "MCP Inspector",
|
||||
},
|
||||
clientInformation: jest.fn().mockImplementation(async () => {
|
||||
const serverUrl = "https://example.com/mcp";
|
||||
const preregisteredKey = `[${serverUrl}] ${SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION}`;
|
||||
const preregisteredData = sessionStorage.getItem(preregisteredKey);
|
||||
if (preregisteredData) {
|
||||
return JSON.parse(preregisteredData);
|
||||
}
|
||||
const dynamicKey = `[${serverUrl}] ${SESSION_KEYS.CLIENT_INFORMATION}`;
|
||||
const dynamicData = sessionStorage.getItem(dynamicKey);
|
||||
if (dynamicData) {
|
||||
return JSON.parse(dynamicData);
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
saveClientInformation: jest.fn().mockImplementation((clientInfo) => {
|
||||
const serverUrl = "https://example.com/mcp";
|
||||
const key = `[${serverUrl}] ${SESSION_KEYS.CLIENT_INFORMATION}`;
|
||||
sessionStorage.setItem(key, JSON.stringify(clientInfo));
|
||||
}),
|
||||
saveTokens: jest.fn(),
|
||||
redirectToAuthorization: jest.fn(),
|
||||
saveCodeVerifier: jest.fn(),
|
||||
codeVerifier: jest.fn(),
|
||||
saveServerMetadata: jest.fn(),
|
||||
getServerMetadata: jest.fn(),
|
||||
})),
|
||||
discoverScopes: jest.fn().mockResolvedValue("read write" as never),
|
||||
}));
|
||||
|
||||
import { discoverScopes } from "@/lib/auth";
|
||||
|
||||
// Type the mocked functions properly
|
||||
const mockDiscoverOAuthMetadata = discoverOAuthMetadata as jest.MockedFunction<
|
||||
typeof discoverOAuthMetadata
|
||||
>;
|
||||
const mockDiscoverAuthorizationServerMetadata =
|
||||
discoverAuthorizationServerMetadata as jest.MockedFunction<
|
||||
typeof discoverAuthorizationServerMetadata
|
||||
>;
|
||||
const mockRegisterClient = registerClient as jest.MockedFunction<
|
||||
typeof registerClient
|
||||
>;
|
||||
|
|
@ -74,6 +127,9 @@ const mockDiscoverOAuthProtectedResourceMetadata =
|
|||
discoverOAuthProtectedResourceMetadata as jest.MockedFunction<
|
||||
typeof discoverOAuthProtectedResourceMetadata
|
||||
>;
|
||||
const mockDiscoverScopes = discoverScopes as jest.MockedFunction<
|
||||
typeof discoverScopes
|
||||
>;
|
||||
|
||||
const sessionStorageMock = {
|
||||
getItem: jest.fn(),
|
||||
|
|
@ -102,7 +158,15 @@ describe("AuthDebugger", () => {
|
|||
// Suppress console errors in tests to avoid JSDOM navigation noise
|
||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata);
|
||||
// Set default mock behaviors with complete OAuth metadata
|
||||
mockDiscoverAuthorizationServerMetadata.mockResolvedValue({
|
||||
issuer: "https://oauth.example.com",
|
||||
authorization_endpoint: "https://oauth.example.com/authorize",
|
||||
token_endpoint: "https://oauth.example.com/token",
|
||||
response_types_supported: ["code"],
|
||||
grant_types_supported: ["authorization_code"],
|
||||
scopes_supported: ["read", "write"],
|
||||
});
|
||||
mockRegisterClient.mockResolvedValue(mockOAuthClientInfo);
|
||||
mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue(
|
||||
new Error("No protected resource metadata found"),
|
||||
|
|
@ -203,7 +267,7 @@ describe("AuthDebugger", () => {
|
|||
});
|
||||
|
||||
// Should first discover and save OAuth metadata
|
||||
expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith(
|
||||
expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledWith(
|
||||
new URL("https://example.com/"),
|
||||
);
|
||||
|
||||
|
|
@ -216,7 +280,7 @@ describe("AuthDebugger", () => {
|
|||
});
|
||||
|
||||
it("should show error when quick OAuth flow fails to discover metadata", async () => {
|
||||
mockDiscoverOAuthMetadata.mockRejectedValue(
|
||||
mockDiscoverAuthorizationServerMetadata.mockRejectedValue(
|
||||
new Error("Metadata discovery failed"),
|
||||
);
|
||||
|
||||
|
|
@ -362,7 +426,7 @@ describe("AuthDebugger", () => {
|
|||
fireEvent.click(screen.getByText("Continue"));
|
||||
});
|
||||
|
||||
expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith(
|
||||
expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledWith(
|
||||
new URL("https://example.com/"),
|
||||
);
|
||||
});
|
||||
|
|
@ -418,13 +482,34 @@ describe("AuthDebugger", () => {
|
|||
await waitFor(() => {
|
||||
expect(updateAuthState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
authorizationUrl: expect.stringContaining("scope="),
|
||||
authorizationUrl: expect.objectContaining({
|
||||
href: "https://oauth.example.com/authorize?scope=read+write",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not include scope in authorization URL when scopes_supported is not present", async () => {
|
||||
it("should include scope in authorization URL when scopes_supported is not present", async () => {
|
||||
const updateAuthState =
|
||||
await setupAuthorizationUrlTest(mockOAuthMetadata);
|
||||
|
||||
// Wait for the updateAuthState to be called
|
||||
await waitFor(() => {
|
||||
expect(updateAuthState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
authorizationUrl: expect.objectContaining({
|
||||
href: "https://oauth.example.com/authorize?scope=read+write",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should omit scope from authorization URL when discoverScopes returns undefined", async () => {
|
||||
// Mock discoverScopes to return undefined (no scopes available)
|
||||
mockDiscoverScopes.mockResolvedValueOnce(undefined);
|
||||
|
||||
const updateAuthState =
|
||||
await setupAuthorizationUrlTest(mockOAuthMetadata);
|
||||
|
||||
|
|
@ -439,6 +524,103 @@ describe("AuthDebugger", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("Client Registration behavior", () => {
|
||||
it("uses preregistered (static) client information without calling DCR", async () => {
|
||||
const preregClientInfo = {
|
||||
client_id: "static_client_id",
|
||||
client_secret: "static_client_secret",
|
||||
redirect_uris: ["http://localhost:3000/oauth/callback/debug"],
|
||||
};
|
||||
|
||||
// Return preregistered client info for the server-specific key
|
||||
sessionStorageMock.getItem.mockImplementation((key) => {
|
||||
if (
|
||||
key ===
|
||||
`[${defaultProps.serverUrl}] ${SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION}`
|
||||
) {
|
||||
return JSON.stringify(preregClientInfo);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const updateAuthState = jest.fn();
|
||||
|
||||
await act(async () => {
|
||||
renderAuthDebugger({
|
||||
updateAuthState,
|
||||
authState: {
|
||||
...defaultAuthState,
|
||||
isInitiatingAuth: false,
|
||||
oauthStep: "client_registration",
|
||||
oauthMetadata: mockOAuthMetadata as unknown as OAuthMetadata,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Proceed from client_registration → authorization_redirect
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText("Continue"));
|
||||
});
|
||||
|
||||
// Should NOT attempt dynamic client registration
|
||||
expect(mockRegisterClient).not.toHaveBeenCalled();
|
||||
|
||||
// Should advance with the preregistered client info
|
||||
expect(updateAuthState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
oauthClientInfo: expect.objectContaining({
|
||||
client_id: "static_client_id",
|
||||
}),
|
||||
oauthStep: "authorization_redirect",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to DCR when no static client information is available", async () => {
|
||||
// No preregistered or dynamic client info present in session storage
|
||||
sessionStorageMock.getItem.mockImplementation(() => null);
|
||||
|
||||
// DCR returns a new client
|
||||
mockRegisterClient.mockResolvedValueOnce(mockOAuthClientInfo);
|
||||
|
||||
const updateAuthState = jest.fn();
|
||||
|
||||
await act(async () => {
|
||||
renderAuthDebugger({
|
||||
updateAuthState,
|
||||
authState: {
|
||||
...defaultAuthState,
|
||||
isInitiatingAuth: false,
|
||||
oauthStep: "client_registration",
|
||||
oauthMetadata: mockOAuthMetadata as unknown as OAuthMetadata,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText("Continue"));
|
||||
});
|
||||
|
||||
expect(mockRegisterClient).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Should save and advance with the DCR client info
|
||||
expect(updateAuthState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
oauthClientInfo: expect.objectContaining({
|
||||
client_id: "test_client_id",
|
||||
}),
|
||||
oauthStep: "authorization_redirect",
|
||||
}),
|
||||
);
|
||||
|
||||
// Verify the dynamically registered client info was persisted
|
||||
expect(sessionStorage.setItem).toHaveBeenCalledWith(
|
||||
`[${defaultProps.serverUrl}] ${SESSION_KEYS.CLIENT_INFORMATION}`,
|
||||
expect.any(String),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("OAuth State Persistence", () => {
|
||||
it("should store auth state to sessionStorage before redirect in Quick OAuth Flow", async () => {
|
||||
const updateAuthState = jest.fn();
|
||||
|
|
@ -509,7 +691,9 @@ describe("AuthDebugger", () => {
|
|||
mockDiscoverOAuthProtectedResourceMetadata.mockResolvedValue(
|
||||
mockResourceMetadata,
|
||||
);
|
||||
mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata);
|
||||
mockDiscoverAuthorizationServerMetadata.mockResolvedValue(
|
||||
mockOAuthMetadata,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
renderAuthDebugger({
|
||||
|
|
@ -563,7 +747,9 @@ describe("AuthDebugger", () => {
|
|||
// Mock failed metadata discovery
|
||||
mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue(mockError);
|
||||
// But OAuth metadata should still work with the original URL
|
||||
mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata);
|
||||
mockDiscoverAuthorizationServerMetadata.mockResolvedValue(
|
||||
mockOAuthMetadata,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
renderAuthDebugger({
|
||||
|
|
@ -603,7 +789,7 @@ describe("AuthDebugger", () => {
|
|||
});
|
||||
|
||||
// Verify that regular OAuth metadata discovery was still called
|
||||
expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith(
|
||||
expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledWith(
|
||||
new URL("https://example.com/"),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -36,6 +36,18 @@ describe("DynamicJsonForm String Fields", () => {
|
|||
const input = screen.getByRole("textbox");
|
||||
expect(input).toHaveProperty("type", "text");
|
||||
});
|
||||
|
||||
it("should handle a union type of string and null", () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: ["string", "null"],
|
||||
description: "Test string or null field",
|
||||
};
|
||||
render(
|
||||
<DynamicJsonForm schema={schema} value={null} onChange={jest.fn()} />,
|
||||
);
|
||||
const input = screen.getByRole("textbox");
|
||||
expect(input).toHaveProperty("type", "text");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Format Support", () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { render, screen, fireEvent, within } from "@testing-library/react";
|
||||
import { useState } from "react";
|
||||
import { describe, it, expect, jest } from "@jest/globals";
|
||||
import HistoryAndNotifications from "../HistoryAndNotifications";
|
||||
import { ServerNotification } from "@modelcontextprotocol/sdk/types.js";
|
||||
|
|
@ -223,4 +224,66 @@ describe("HistoryAndNotifications", () => {
|
|||
expect(screen.getByText("No history yet")).toBeTruthy();
|
||||
expect(screen.getByText("No notifications yet")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("clears request history when Clear is clicked", () => {
|
||||
const Wrapper = () => {
|
||||
const [history, setHistory] = useState(mockRequestHistory);
|
||||
return (
|
||||
<HistoryAndNotifications
|
||||
requestHistory={history}
|
||||
serverNotifications={[]}
|
||||
onClearHistory={() => setHistory([])}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render(<Wrapper />);
|
||||
|
||||
// Verify items are present initially
|
||||
expect(screen.getByText("2. test/method2")).toBeTruthy();
|
||||
expect(screen.getByText("1. test/method1")).toBeTruthy();
|
||||
|
||||
// Click Clear in History header (scoped by the History heading's container)
|
||||
const historyHeader = screen.getByText("History");
|
||||
const historyHeaderContainer = historyHeader.parentElement as HTMLElement;
|
||||
const historyClearButton = within(historyHeaderContainer).getByRole(
|
||||
"button",
|
||||
{ name: "Clear" },
|
||||
);
|
||||
fireEvent.click(historyClearButton);
|
||||
|
||||
// History should now be empty
|
||||
expect(screen.getByText("No history yet")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("clears server notifications when Clear is clicked", () => {
|
||||
const Wrapper = () => {
|
||||
const [notifications, setNotifications] =
|
||||
useState<ServerNotification[]>(mockNotifications);
|
||||
return (
|
||||
<HistoryAndNotifications
|
||||
requestHistory={[]}
|
||||
serverNotifications={notifications}
|
||||
onClearNotifications={() => setNotifications([])}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render(<Wrapper />);
|
||||
|
||||
// Verify items are present initially
|
||||
expect(screen.getByText("2. notifications/progress")).toBeTruthy();
|
||||
expect(screen.getByText("1. notifications/message")).toBeTruthy();
|
||||
|
||||
// Click Clear in Server Notifications header (scoped by its heading's container)
|
||||
const notifHeader = screen.getByText("Server Notifications");
|
||||
const notifHeaderContainer = notifHeader.parentElement as HTMLElement;
|
||||
const notifClearButton = within(notifHeaderContainer).getByRole("button", {
|
||||
name: "Clear",
|
||||
});
|
||||
fireEvent.click(notifClearButton);
|
||||
|
||||
// Notifications should now be empty
|
||||
expect(screen.getByText("No notifications yet")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
203
client/src/components/__tests__/ListPane.test.tsx
Normal file
203
client/src/components/__tests__/ListPane.test.tsx
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
import { render, screen, fireEvent, act } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom";
|
||||
import { describe, it, beforeEach, jest } from "@jest/globals";
|
||||
import ListPane from "../ListPane";
|
||||
|
||||
describe("ListPane", () => {
|
||||
const mockItems = [
|
||||
{ id: 1, name: "Tool 1", description: "First tool" },
|
||||
{ id: 2, name: "Tool 2", description: "Second tool" },
|
||||
{ id: 3, name: "Another Tool", description: "Third tool" },
|
||||
];
|
||||
|
||||
const defaultProps = {
|
||||
items: mockItems,
|
||||
listItems: jest.fn(),
|
||||
clearItems: jest.fn(),
|
||||
setSelectedItem: jest.fn(),
|
||||
renderItem: (item: (typeof mockItems)[0]) => <div>{item.name}</div>,
|
||||
title: "List tools",
|
||||
buttonText: "Load Tools",
|
||||
};
|
||||
|
||||
const renderListPane = (props = {}) => {
|
||||
return render(<ListPane {...defaultProps} {...props} />);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("Rendering", () => {
|
||||
it("should render with title and button", () => {
|
||||
renderListPane();
|
||||
|
||||
expect(screen.getByText("List tools")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Load Tools" }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Clear" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render items when provided", () => {
|
||||
renderListPane();
|
||||
|
||||
expect(screen.getByText("Tool 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Tool 2")).toBeInTheDocument();
|
||||
expect(screen.getByText("Another Tool")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render empty state when no items", () => {
|
||||
renderListPane({ items: [] });
|
||||
|
||||
expect(screen.queryByText("Tool 1")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Tool 2")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render custom item content", () => {
|
||||
const customRenderItem = (item: (typeof mockItems)[0]) => (
|
||||
<div>
|
||||
<span>{item.name}</span>
|
||||
<small>{item.description}</small>
|
||||
</div>
|
||||
);
|
||||
|
||||
renderListPane({ renderItem: customRenderItem });
|
||||
|
||||
expect(screen.getByText("Tool 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("First tool")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Search Functionality", () => {
|
||||
it("should show search icon initially", () => {
|
||||
renderListPane();
|
||||
|
||||
const searchButton = screen.getByRole("button", { name: "Search" });
|
||||
expect(searchButton).toBeInTheDocument();
|
||||
expect(searchButton.querySelector("svg")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should expand search input when search icon is clicked", async () => {
|
||||
renderListPane();
|
||||
|
||||
const searchButton = screen.getByRole("button", { name: "Search" });
|
||||
await act(async () => {
|
||||
fireEvent.click(searchButton);
|
||||
});
|
||||
|
||||
const searchInput = screen.getByPlaceholderText("Search...");
|
||||
expect(searchInput).toBeInTheDocument();
|
||||
|
||||
// Wait for the setTimeout to complete and focus to be set
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||
});
|
||||
|
||||
expect(searchInput).toHaveFocus();
|
||||
});
|
||||
|
||||
it("should filter items based on search query", async () => {
|
||||
renderListPane();
|
||||
|
||||
const searchButton = screen.getByRole("button", { name: "Search" });
|
||||
await act(async () => {
|
||||
fireEvent.click(searchButton);
|
||||
});
|
||||
|
||||
const searchInput = screen.getByPlaceholderText("Search...");
|
||||
await act(async () => {
|
||||
fireEvent.change(searchInput, { target: { value: "Tool" } });
|
||||
});
|
||||
|
||||
expect(screen.getByText("Tool 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Tool 2")).toBeInTheDocument();
|
||||
expect(screen.getByText("Another Tool")).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(searchInput, { target: { value: "Another" } });
|
||||
});
|
||||
|
||||
expect(screen.queryByText("Tool 1")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Tool 2")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Another Tool")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show 'No items found of matching \"NonExistent\"' when search has no results", async () => {
|
||||
renderListPane();
|
||||
|
||||
const searchButton = screen.getByRole("button", { name: "Search" });
|
||||
await act(async () => {
|
||||
fireEvent.click(searchButton);
|
||||
});
|
||||
|
||||
const searchInput = screen.getByPlaceholderText("Search...");
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(searchInput, { target: { value: "NonExistent" } });
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText('No items found matching "NonExistent"'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByText("Tool 1")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should collapse search when input is empty and loses focus", async () => {
|
||||
renderListPane();
|
||||
|
||||
const searchButton = screen.getByRole("button", { name: "Search" });
|
||||
await act(async () => {
|
||||
fireEvent.click(searchButton);
|
||||
});
|
||||
|
||||
const searchInput = screen.getByPlaceholderText("Search...");
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(searchInput, { target: { value: "test" } });
|
||||
fireEvent.change(searchInput, { target: { value: "" } });
|
||||
fireEvent.blur(searchInput);
|
||||
});
|
||||
|
||||
const searchButtonAfterCollapse = screen.getByRole("button", {
|
||||
name: "Search",
|
||||
});
|
||||
expect(searchButtonAfterCollapse).toBeInTheDocument();
|
||||
expect(searchButtonAfterCollapse).not.toHaveClass("opacity-0");
|
||||
});
|
||||
|
||||
it("should keep search expanded when input has content and loses focus", async () => {
|
||||
renderListPane();
|
||||
|
||||
const searchButton = screen.getByRole("button", { name: "Search" });
|
||||
await act(async () => {
|
||||
fireEvent.click(searchButton);
|
||||
});
|
||||
|
||||
const searchInput = screen.getByPlaceholderText("Search...");
|
||||
await act(async () => {
|
||||
fireEvent.change(searchInput, { target: { value: "test" } });
|
||||
fireEvent.blur(searchInput);
|
||||
});
|
||||
|
||||
expect(screen.getByPlaceholderText("Search...")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should search through all item properties (description)", async () => {
|
||||
renderListPane();
|
||||
|
||||
const searchButton = screen.getByRole("button", { name: "Search" });
|
||||
await act(async () => {
|
||||
fireEvent.click(searchButton);
|
||||
});
|
||||
|
||||
const searchInput = screen.getByPlaceholderText("Search...");
|
||||
await act(async () => {
|
||||
fireEvent.change(searchInput, { target: { value: "First tool" } });
|
||||
});
|
||||
|
||||
expect(screen.getByText("Tool 1")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Tool 2")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue