Merge branch 'main' into fix-required
This commit is contained in:
commit
6f80feb0b2
2 changed files with 902 additions and 218 deletions
|
|
@ -40,6 +40,7 @@ jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
|
|||
registerClient: jest.fn(),
|
||||
startAuthorization: jest.fn(),
|
||||
exchangeAuthorization: jest.fn(),
|
||||
discoverOAuthProtectedResourceMetadata: jest.fn(),
|
||||
}));
|
||||
|
||||
// Import the functions to get their types
|
||||
|
|
@ -49,8 +50,10 @@ import {
|
|||
startAuthorization,
|
||||
exchangeAuthorization,
|
||||
auth,
|
||||
discoverOAuthProtectedResourceMetadata,
|
||||
} from "@modelcontextprotocol/sdk/client/auth.js";
|
||||
import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
|
||||
import { EMPTY_DEBUGGER_STATE } from "@/lib/auth-types";
|
||||
|
||||
// Type the mocked functions properly
|
||||
const mockDiscoverOAuthMetadata = discoverOAuthMetadata as jest.MockedFunction<
|
||||
|
|
@ -66,6 +69,10 @@ const mockExchangeAuthorization = exchangeAuthorization as jest.MockedFunction<
|
|||
typeof exchangeAuthorization
|
||||
>;
|
||||
const mockAuth = auth as jest.MockedFunction<typeof auth>;
|
||||
const mockDiscoverOAuthProtectedResourceMetadata =
|
||||
discoverOAuthProtectedResourceMetadata as jest.MockedFunction<
|
||||
typeof discoverOAuthProtectedResourceMetadata
|
||||
>;
|
||||
|
||||
const sessionStorageMock = {
|
||||
getItem: jest.fn(),
|
||||
|
|
@ -84,19 +91,7 @@ Object.defineProperty(window, "location", {
|
|||
});
|
||||
|
||||
describe("AuthDebugger", () => {
|
||||
const defaultAuthState = {
|
||||
isInitiatingAuth: false,
|
||||
oauthTokens: null,
|
||||
loading: false,
|
||||
oauthStep: "metadata_discovery" as const,
|
||||
oauthMetadata: null,
|
||||
oauthClientInfo: null,
|
||||
authorizationUrl: null,
|
||||
authorizationCode: "",
|
||||
latestError: null,
|
||||
statusMessage: null,
|
||||
validationError: null,
|
||||
};
|
||||
const defaultAuthState = EMPTY_DEBUGGER_STATE;
|
||||
|
||||
const defaultProps = {
|
||||
serverUrl: "https://example.com",
|
||||
|
|
@ -109,8 +104,14 @@ describe("AuthDebugger", () => {
|
|||
jest.clearAllMocks();
|
||||
sessionStorageMock.getItem.mockReturnValue(null);
|
||||
|
||||
// Supress
|
||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata);
|
||||
mockRegisterClient.mockResolvedValue(mockOAuthClientInfo);
|
||||
mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue(
|
||||
new Error("No protected resource metadata found"),
|
||||
);
|
||||
mockStartAuthorization.mockImplementation(async (_sseUrl, options) => {
|
||||
const authUrl = new URL("https://oauth.example.com/authorize");
|
||||
|
||||
|
|
@ -126,6 +127,10 @@ describe("AuthDebugger", () => {
|
|||
mockExchangeAuthorization.mockResolvedValue(mockOAuthTokens);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const renderAuthDebugger = (props: Partial<AuthDebuggerProps> = {}) => {
|
||||
const mergedProps = {
|
||||
...defaultProps,
|
||||
|
|
@ -204,7 +209,7 @@ describe("AuthDebugger", () => {
|
|||
|
||||
// Should first discover and save OAuth metadata
|
||||
expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith(
|
||||
"https://example.com",
|
||||
new URL("https://example.com"),
|
||||
);
|
||||
|
||||
// Check that updateAuthState was called with the right info message
|
||||
|
|
@ -316,6 +321,11 @@ describe("AuthDebugger", () => {
|
|||
});
|
||||
|
||||
expect(updateAuthState).toHaveBeenCalledWith({
|
||||
authServerUrl: null,
|
||||
authorizationUrl: null,
|
||||
isInitiatingAuth: false,
|
||||
resourceMetadata: null,
|
||||
resourceMetadataError: null,
|
||||
oauthTokens: null,
|
||||
oauthStep: "metadata_discovery",
|
||||
latestError: null,
|
||||
|
|
@ -357,7 +367,7 @@ describe("AuthDebugger", () => {
|
|||
});
|
||||
|
||||
expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith(
|
||||
"https://example.com",
|
||||
new URL("https://example.com"),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -432,4 +442,186 @@ describe("AuthDebugger", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("OAuth State Persistence", () => {
|
||||
it("should store auth state to sessionStorage before redirect in Quick OAuth Flow", async () => {
|
||||
const updateAuthState = jest.fn();
|
||||
|
||||
// Mock window.location.href setter
|
||||
const originalLocation = window.location;
|
||||
const locationMock = {
|
||||
...originalLocation,
|
||||
href: "",
|
||||
origin: "http://localhost:3000",
|
||||
};
|
||||
Object.defineProperty(window, "location", {
|
||||
writable: true,
|
||||
value: locationMock,
|
||||
});
|
||||
|
||||
// Setup mocks for OAuth flow
|
||||
mockStartAuthorization.mockResolvedValue({
|
||||
authorizationUrl: new URL(
|
||||
"https://oauth.example.com/authorize?client_id=test_client_id&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Foauth%2Fcallback%2Fdebug",
|
||||
),
|
||||
codeVerifier: "test_verifier",
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
renderAuthDebugger({
|
||||
updateAuthState,
|
||||
authState: { ...defaultAuthState },
|
||||
});
|
||||
});
|
||||
|
||||
// Click Quick OAuth Flow
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText("Quick OAuth Flow"));
|
||||
});
|
||||
|
||||
// Wait for the flow to reach the authorization step
|
||||
await waitFor(() => {
|
||||
expect(sessionStorage.setItem).toHaveBeenCalledWith(
|
||||
SESSION_KEYS.AUTH_DEBUGGER_STATE,
|
||||
expect.stringContaining('"oauthStep":"authorization_code"'),
|
||||
);
|
||||
});
|
||||
|
||||
// Verify the stored state includes all the accumulated data
|
||||
const storedStateCall = (
|
||||
sessionStorage.setItem as jest.Mock
|
||||
).mock.calls.find((call) => call[0] === SESSION_KEYS.AUTH_DEBUGGER_STATE);
|
||||
|
||||
expect(storedStateCall).toBeDefined();
|
||||
const storedState = JSON.parse(storedStateCall![1] as string);
|
||||
|
||||
expect(storedState).toMatchObject({
|
||||
oauthStep: "authorization_code",
|
||||
authorizationUrl: expect.stringMatching(
|
||||
/^https:\/\/oauth\.example\.com\/authorize/,
|
||||
),
|
||||
oauthMetadata: expect.objectContaining({
|
||||
token_endpoint: "https://oauth.example.com/token",
|
||||
}),
|
||||
oauthClientInfo: expect.objectContaining({
|
||||
client_id: "test_client_id",
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("OAuth Protected Resource Metadata", () => {
|
||||
it("should successfully fetch and display protected resource metadata", async () => {
|
||||
const updateAuthState = jest.fn();
|
||||
const mockResourceMetadata = {
|
||||
resource: "https://example.com/api",
|
||||
authorization_servers: ["https://custom-auth.example.com"],
|
||||
bearer_methods_supported: ["header", "body"],
|
||||
resource_documentation: "https://example.com/api/docs",
|
||||
resource_policy_uri: "https://example.com/api/policy",
|
||||
};
|
||||
|
||||
// Mock successful metadata discovery
|
||||
mockDiscoverOAuthProtectedResourceMetadata.mockResolvedValue(
|
||||
mockResourceMetadata,
|
||||
);
|
||||
mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata);
|
||||
|
||||
await act(async () => {
|
||||
renderAuthDebugger({
|
||||
updateAuthState,
|
||||
authState: { ...defaultAuthState },
|
||||
});
|
||||
});
|
||||
|
||||
// Click Guided OAuth Flow to start the process
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText("Guided OAuth Flow"));
|
||||
});
|
||||
|
||||
// Verify that the flow started with metadata discovery
|
||||
expect(updateAuthState).toHaveBeenCalledWith({
|
||||
oauthStep: "metadata_discovery",
|
||||
authorizationUrl: null,
|
||||
statusMessage: null,
|
||||
latestError: null,
|
||||
});
|
||||
|
||||
// Click Continue to trigger metadata discovery
|
||||
const continueButton = await screen.findByText("Continue");
|
||||
await act(async () => {
|
||||
fireEvent.click(continueButton);
|
||||
});
|
||||
|
||||
// Wait for the metadata to be fetched
|
||||
await waitFor(() => {
|
||||
expect(mockDiscoverOAuthProtectedResourceMetadata).toHaveBeenCalledWith(
|
||||
"https://example.com",
|
||||
);
|
||||
});
|
||||
|
||||
// Verify the state was updated with the resource metadata
|
||||
await waitFor(() => {
|
||||
expect(updateAuthState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
resourceMetadata: mockResourceMetadata,
|
||||
authServerUrl: new URL("https://custom-auth.example.com"),
|
||||
oauthStep: "client_registration",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle protected resource metadata fetch failure gracefully", async () => {
|
||||
const updateAuthState = jest.fn();
|
||||
const mockError = new Error("Failed to fetch resource metadata");
|
||||
|
||||
// Mock failed metadata discovery
|
||||
mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue(mockError);
|
||||
// But OAuth metadata should still work with the original URL
|
||||
mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata);
|
||||
|
||||
await act(async () => {
|
||||
renderAuthDebugger({
|
||||
updateAuthState,
|
||||
authState: { ...defaultAuthState },
|
||||
});
|
||||
});
|
||||
|
||||
// Click Guided OAuth Flow
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText("Guided OAuth Flow"));
|
||||
});
|
||||
|
||||
// Click Continue to trigger metadata discovery
|
||||
const continueButton = await screen.findByText("Continue");
|
||||
await act(async () => {
|
||||
fireEvent.click(continueButton);
|
||||
});
|
||||
|
||||
// Wait for the metadata fetch to fail
|
||||
await waitFor(() => {
|
||||
expect(mockDiscoverOAuthProtectedResourceMetadata).toHaveBeenCalledWith(
|
||||
"https://example.com",
|
||||
);
|
||||
});
|
||||
|
||||
// Verify the flow continues despite the error
|
||||
await waitFor(() => {
|
||||
expect(updateAuthState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
resourceMetadataError: mockError,
|
||||
// Should use the original server URL as fallback
|
||||
authServerUrl: new URL("https://example.com"),
|
||||
oauthStep: "client_registration",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// Verify that regular OAuth metadata discovery was still called
|
||||
expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith(
|
||||
new URL("https://example.com"),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
308
client/src/components/__tests__/ResourcesTab.test.tsx
Normal file
308
client/src/components/__tests__/ResourcesTab.test.tsx
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom";
|
||||
import { Tabs } from "@/components/ui/tabs";
|
||||
import ResourcesTab from "../ResourcesTab";
|
||||
import { ResourceTemplate, Resource } from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
// Mock the hooks and components
|
||||
jest.mock("@/lib/hooks/useCompletionState", () => ({
|
||||
useCompletionState: () => ({
|
||||
completions: {},
|
||||
clearCompletions: jest.fn(),
|
||||
requestCompletions: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock("../JsonView", () => {
|
||||
return function MockJsonView({ data }: { data: string }) {
|
||||
return <div data-testid="json-view">{data}</div>;
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock("@/components/ui/combobox", () => ({
|
||||
Combobox: ({
|
||||
id,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
}: {
|
||||
id: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder: string;
|
||||
}) => (
|
||||
<input
|
||||
id={id}
|
||||
value={value || ""}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
data-testid={`combobox-${id}`}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("@/components/ui/label", () => ({
|
||||
Label: ({
|
||||
htmlFor,
|
||||
children,
|
||||
}: {
|
||||
htmlFor: string;
|
||||
children: React.ReactNode;
|
||||
}) => (
|
||||
<label htmlFor={htmlFor} data-testid={`label-${htmlFor}`}>
|
||||
{children}
|
||||
</label>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("@/components/ui/button", () => ({
|
||||
Button: ({
|
||||
children,
|
||||
onClick,
|
||||
disabled,
|
||||
...props
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
[key: string]: unknown;
|
||||
}) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
data-testid="button"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("ResourcesTab - Template Query Parameters", () => {
|
||||
const mockListResources = jest.fn();
|
||||
const mockClearResources = jest.fn();
|
||||
const mockListResourceTemplates = jest.fn();
|
||||
const mockClearResourceTemplates = jest.fn();
|
||||
const mockReadResource = jest.fn();
|
||||
const mockSetSelectedResource = jest.fn();
|
||||
const mockHandleCompletion = jest.fn();
|
||||
const mockSubscribeToResource = jest.fn();
|
||||
const mockUnsubscribeFromResource = jest.fn();
|
||||
|
||||
const mockResourceTemplate: ResourceTemplate = {
|
||||
name: "Users API",
|
||||
uriTemplate: "test://users{?name,limit,offset}",
|
||||
description: "Fetch users with optional filtering and pagination",
|
||||
};
|
||||
|
||||
const mockResource: Resource = {
|
||||
uri: "test://users?name=john&limit=10&offset=0",
|
||||
name: "Users Resource",
|
||||
description: "Expanded users resource",
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
resources: [],
|
||||
resourceTemplates: [mockResourceTemplate],
|
||||
listResources: mockListResources,
|
||||
clearResources: mockClearResources,
|
||||
listResourceTemplates: mockListResourceTemplates,
|
||||
clearResourceTemplates: mockClearResourceTemplates,
|
||||
readResource: mockReadResource,
|
||||
selectedResource: null,
|
||||
setSelectedResource: mockSetSelectedResource,
|
||||
handleCompletion: mockHandleCompletion,
|
||||
completionsSupported: true,
|
||||
resourceContent: "",
|
||||
nextCursor: undefined,
|
||||
nextTemplateCursor: undefined,
|
||||
error: null,
|
||||
resourceSubscriptionsSupported: false,
|
||||
resourceSubscriptions: new Set<string>(),
|
||||
subscribeToResource: mockSubscribeToResource,
|
||||
unsubscribeFromResource: mockUnsubscribeFromResource,
|
||||
};
|
||||
|
||||
const renderResourcesTab = (props = {}) =>
|
||||
render(
|
||||
<Tabs defaultValue="resources">
|
||||
<ResourcesTab {...defaultProps} {...props} />
|
||||
</Tabs>,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should parse and display template variables from URI template", () => {
|
||||
renderResourcesTab();
|
||||
|
||||
// Click on the resource template to select it
|
||||
fireEvent.click(screen.getByText("Users API"));
|
||||
|
||||
// Check that input fields are rendered for each template variable
|
||||
expect(screen.getByTestId("combobox-name")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("combobox-limit")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("combobox-offset")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display template description when template is selected", () => {
|
||||
renderResourcesTab();
|
||||
|
||||
// Click on the resource template to select it
|
||||
fireEvent.click(screen.getByText("Users API"));
|
||||
|
||||
expect(
|
||||
screen.getByText("Fetch users with optional filtering and pagination"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle template value changes", () => {
|
||||
renderResourcesTab();
|
||||
|
||||
// Click on the resource template to select it
|
||||
fireEvent.click(screen.getByText("Users API"));
|
||||
|
||||
// Find and fill template value inputs
|
||||
const nameInput = screen.getByTestId("combobox-name");
|
||||
const limitInput = screen.getByTestId("combobox-limit");
|
||||
const offsetInput = screen.getByTestId("combobox-offset");
|
||||
|
||||
fireEvent.change(nameInput, { target: { value: "john" } });
|
||||
fireEvent.change(limitInput, { target: { value: "10" } });
|
||||
fireEvent.change(offsetInput, { target: { value: "0" } });
|
||||
|
||||
expect(nameInput).toHaveValue("john");
|
||||
expect(limitInput).toHaveValue("10");
|
||||
expect(offsetInput).toHaveValue("0");
|
||||
});
|
||||
|
||||
it("should expand template and read resource when Read Resource button is clicked", async () => {
|
||||
renderResourcesTab();
|
||||
|
||||
// Click on the resource template to select it
|
||||
fireEvent.click(screen.getByText("Users API"));
|
||||
|
||||
// Fill template values
|
||||
const nameInput = screen.getByTestId("combobox-name");
|
||||
const limitInput = screen.getByTestId("combobox-limit");
|
||||
const offsetInput = screen.getByTestId("combobox-offset");
|
||||
|
||||
fireEvent.change(nameInput, { target: { value: "john" } });
|
||||
fireEvent.change(limitInput, { target: { value: "10" } });
|
||||
fireEvent.change(offsetInput, { target: { value: "0" } });
|
||||
|
||||
// Click Read Resource button
|
||||
const readResourceButton = screen.getByText("Read Resource");
|
||||
expect(readResourceButton).not.toBeDisabled();
|
||||
|
||||
fireEvent.click(readResourceButton);
|
||||
|
||||
// Verify that readResource was called with the expanded URI
|
||||
expect(mockReadResource).toHaveBeenCalledWith(
|
||||
"test://users?name=john&limit=10&offset=0",
|
||||
);
|
||||
|
||||
// Verify that setSelectedResource was called with the expanded resource
|
||||
expect(mockSetSelectedResource).toHaveBeenCalledWith({
|
||||
uri: "test://users?name=john&limit=10&offset=0",
|
||||
name: "test://users?name=john&limit=10&offset=0",
|
||||
});
|
||||
});
|
||||
|
||||
it("should disable Read Resource button when no template values are provided", () => {
|
||||
renderResourcesTab();
|
||||
|
||||
// Click on the resource template to select it
|
||||
fireEvent.click(screen.getByText("Users API"));
|
||||
|
||||
// Read Resource button should be disabled when no values are provided
|
||||
const readResourceButton = screen.getByText("Read Resource");
|
||||
expect(readResourceButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should handle partial template values correctly", () => {
|
||||
renderResourcesTab();
|
||||
|
||||
// Click on the resource template to select it
|
||||
fireEvent.click(screen.getByText("Users API"));
|
||||
|
||||
// Fill only some template values
|
||||
const nameInput = screen.getByTestId("combobox-name");
|
||||
fireEvent.change(nameInput, { target: { value: "john" } });
|
||||
|
||||
// Read Resource button should be enabled with partial values
|
||||
const readResourceButton = screen.getByText("Read Resource");
|
||||
expect(readResourceButton).not.toBeDisabled();
|
||||
|
||||
fireEvent.click(readResourceButton);
|
||||
|
||||
// Should expand with only the provided values
|
||||
expect(mockReadResource).toHaveBeenCalledWith("test://users?name=john");
|
||||
});
|
||||
|
||||
it("should handle special characters in template values", () => {
|
||||
renderResourcesTab();
|
||||
|
||||
// Click on the resource template to select it
|
||||
fireEvent.click(screen.getByText("Users API"));
|
||||
|
||||
// Fill template values with special characters
|
||||
const nameInput = screen.getByTestId("combobox-name");
|
||||
fireEvent.change(nameInput, { target: { value: "john doe" } });
|
||||
|
||||
fireEvent.click(screen.getByText("Read Resource"));
|
||||
|
||||
// Should properly encode special characters
|
||||
expect(mockReadResource).toHaveBeenCalledWith(
|
||||
"test://users?name=john%20doe",
|
||||
);
|
||||
});
|
||||
|
||||
it("should clear template values when switching between templates", () => {
|
||||
const anotherTemplate: ResourceTemplate = {
|
||||
name: "Posts API",
|
||||
uriTemplate: "test://posts{?author,category}",
|
||||
description: "Fetch posts by author and category",
|
||||
};
|
||||
|
||||
renderResourcesTab({
|
||||
resourceTemplates: [mockResourceTemplate, anotherTemplate],
|
||||
});
|
||||
|
||||
// Select first template and fill values
|
||||
fireEvent.click(screen.getByText("Users API"));
|
||||
const nameInput = screen.getByTestId("combobox-name");
|
||||
fireEvent.change(nameInput, { target: { value: "john" } });
|
||||
|
||||
// Switch to second template
|
||||
fireEvent.click(screen.getByText("Posts API"));
|
||||
|
||||
// Should show new template fields and clear previous values
|
||||
expect(screen.getByTestId("combobox-author")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("combobox-category")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("combobox-name")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display resource content when a resource is selected", () => {
|
||||
const resourceContent = '{"users": [{"id": 1, "name": "John"}]}';
|
||||
|
||||
renderResourcesTab({
|
||||
selectedResource: mockResource,
|
||||
resourceContent: resourceContent,
|
||||
});
|
||||
|
||||
expect(screen.getByTestId("json-view")).toBeInTheDocument();
|
||||
expect(screen.getByText(resourceContent)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show alert when no resource or template is selected", () => {
|
||||
renderResourcesTab();
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Select a resource or template from the list to view its contents",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue