test: add comprehensive tests for tool workshop functionality
Backend tests: - Unit tests for docker_build service (successful/failed builds, context, paths) - Unit tests for readiness_probe service (success, timeout, retries, edge cases) - Integration tests for config_folders API (CRUD + project overrides) - Integration tests for tool_types API with new fields - Integration tests for tool_configs API with new fields Frontend tests: - ToolWorkshopPage component tests (all 3 tabs, create/edit/delete) - API client tests for tool_types and config_folders Fixes: - Add field_validator import to tool_configs.py - Add JSON import to tool_config model - Update frontend test button names to match UI (Create Tool Type, Add Config, Create Folder) Quality gates: backend unit tests passing (23/23)
This commit is contained in:
@@ -0,0 +1,558 @@
|
||||
import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { ToolWorkshopPage } from "./tool-workshop";
|
||||
import * as toolTypesApi from "../api/tool_types";
|
||||
import * as toolConfigsApi from "../api/tool_configs";
|
||||
import * as configFoldersApi from "../api/config_folders";
|
||||
|
||||
const mockToolTypes = [
|
||||
{
|
||||
id: "type-1",
|
||||
name: "code-server",
|
||||
display_name: "VS Code Server",
|
||||
description: "VS Code in browser",
|
||||
category: "editor",
|
||||
interfaces: ["web"],
|
||||
default_port: 8443,
|
||||
definition_type: "compose",
|
||||
compose_template: "version: '3.8'\\nservices:\\n app:\\n image: codercom/code-server",
|
||||
dockerfile_template: null,
|
||||
readiness_probe: null,
|
||||
required_variables: ["REPO_PATH"],
|
||||
is_builtin: true,
|
||||
created_by_id: null,
|
||||
},
|
||||
{
|
||||
id: "type-2",
|
||||
name: "custom-tool",
|
||||
display_name: "Custom Tool",
|
||||
description: "My custom tool",
|
||||
category: "utility",
|
||||
interfaces: ["terminal"],
|
||||
default_port: 8080,
|
||||
definition_type: "dockerfile",
|
||||
compose_template: null,
|
||||
dockerfile_template: "FROM python:3.11",
|
||||
readiness_probe: {
|
||||
command: "python --version",
|
||||
timeout: 30,
|
||||
interval: 2,
|
||||
},
|
||||
required_variables: [],
|
||||
is_builtin: false,
|
||||
created_by_id: "user-1",
|
||||
},
|
||||
];
|
||||
|
||||
const mockConfigs = [
|
||||
{
|
||||
id: "config-1",
|
||||
tool_type_id: "type-1",
|
||||
key: "OPENAI_API_KEY",
|
||||
value: "sk-test123",
|
||||
config_type: "env",
|
||||
file_path: null,
|
||||
port_override: null,
|
||||
start_command: null,
|
||||
working_directory: null,
|
||||
environment_variables: {},
|
||||
volumes: [],
|
||||
},
|
||||
{
|
||||
id: "config-2",
|
||||
tool_type_id: "type-2",
|
||||
key: "advanced-config",
|
||||
value: "test-value",
|
||||
config_type: "env",
|
||||
port_override: 9090,
|
||||
start_command: "python app.py",
|
||||
working_directory: "/app",
|
||||
environment_variables: { DEBUG: "true" },
|
||||
volumes: [{ source: "data", target: "/data", type: "bind" }],
|
||||
},
|
||||
];
|
||||
|
||||
const mockFolders = [
|
||||
{
|
||||
id: "folder-1",
|
||||
name: "my-dotfiles",
|
||||
description: "My personal config files",
|
||||
mount_path: "/home/user",
|
||||
files: { ".zshrc": "export ZSH=\"$HOME/.oh-my-zsh\"" },
|
||||
project_overrides: {},
|
||||
is_active: true,
|
||||
},
|
||||
{
|
||||
id: "folder-2",
|
||||
name: "project-configs",
|
||||
description: "Project specific configs",
|
||||
mount_path: "/workspace",
|
||||
files: { ".env": "API_URL=http://localhost:8080" },
|
||||
project_overrides: {
|
||||
"proj-1": {
|
||||
mount_path: "/app",
|
||||
files: { ".env": "API_URL=http://prod.api" },
|
||||
},
|
||||
},
|
||||
is_active: false,
|
||||
},
|
||||
];
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("ToolWorkshopPage", () => {
|
||||
it("renders loading state initially", () => {
|
||||
vi.spyOn(toolTypesApi, "listToolTypes").mockImplementation(() => new Promise(() => {}));
|
||||
vi.spyOn(toolConfigsApi, "listToolConfigs").mockImplementation(() => new Promise(() => {}));
|
||||
vi.spyOn(configFoldersApi, "listConfigFolders").mockImplementation(() => new Promise(() => {}));
|
||||
|
||||
render(<ToolWorkshopPage />);
|
||||
expect(screen.getByText(/loading/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders tool types tab by default", async () => {
|
||||
vi.spyOn(toolTypesApi, "listToolTypes").mockResolvedValue(mockToolTypes);
|
||||
vi.spyOn(toolConfigsApi, "listToolConfigs").mockResolvedValue(mockConfigs);
|
||||
vi.spyOn(configFoldersApi, "listConfigFolders").mockResolvedValue(mockFolders);
|
||||
|
||||
render(<ToolWorkshopPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("VS Code Server")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText("Custom Tool")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("switches to configs tab", async () => {
|
||||
vi.spyOn(toolTypesApi, "listToolTypes").mockResolvedValue(mockToolTypes);
|
||||
vi.spyOn(toolConfigsApi, "listToolConfigs").mockResolvedValue(mockConfigs);
|
||||
vi.spyOn(configFoldersApi, "listConfigFolders").mockResolvedValue(mockFolders);
|
||||
|
||||
render(<ToolWorkshopPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("VS Code Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /configs/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("OPENAI_API_KEY")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText("advanced-config")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("switches to folders tab", async () => {
|
||||
vi.spyOn(toolTypesApi, "listToolTypes").mockResolvedValue(mockToolTypes);
|
||||
vi.spyOn(toolConfigsApi, "listToolConfigs").mockResolvedValue(mockConfigs);
|
||||
vi.spyOn(configFoldersApi, "listConfigFolders").mockResolvedValue(mockFolders);
|
||||
|
||||
render(<ToolWorkshopPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("VS Code Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /folders/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("my-dotfiles")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText("project-configs")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens tool type creation form", async () => {
|
||||
vi.spyOn(toolTypesApi, "listToolTypes").mockResolvedValue(mockToolTypes);
|
||||
vi.spyOn(toolConfigsApi, "listToolConfigs").mockResolvedValue(mockConfigs);
|
||||
vi.spyOn(configFoldersApi, "listConfigFolders").mockResolvedValue(mockFolders);
|
||||
|
||||
render(<ToolWorkshopPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("VS Code Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /create tool type/i }));
|
||||
|
||||
expect(screen.getByLabelText(/name/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/display name/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("creates tool type with compose definition", async () => {
|
||||
const listMock = vi.spyOn(toolTypesApi, "listToolTypes").mockResolvedValue(mockToolTypes);
|
||||
const createMock = vi.spyOn(toolTypesApi, "createToolType").mockResolvedValue(mockToolTypes[1]);
|
||||
vi.spyOn(toolConfigsApi, "listToolConfigs").mockResolvedValue(mockConfigs);
|
||||
vi.spyOn(configFoldersApi, "listConfigFolders").mockResolvedValue(mockFolders);
|
||||
|
||||
render(<ToolWorkshopPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("VS Code Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /create tool type/i }));
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/name/i), {
|
||||
target: { value: "new-tool" },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText(/display name/i), {
|
||||
target: { value: "New Tool" },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText(/compose template/i), {
|
||||
target: { value: "version: '3.8'\\nservices:\\n app:\\n image: nginx" },
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /create$/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: "new-tool",
|
||||
display_name: "New Tool",
|
||||
definition_type: "compose",
|
||||
compose_template: "version: '3.8'\\nservices:\\n app:\\n image: nginx",
|
||||
})
|
||||
);
|
||||
});
|
||||
expect(listMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("creates tool type with dockerfile definition", async () => {
|
||||
const listMock = vi.spyOn(toolTypesApi, "listToolTypes").mockResolvedValue(mockToolTypes);
|
||||
const createMock = vi.spyOn(toolTypesApi, "createToolType").mockResolvedValue(mockToolTypes[1]);
|
||||
vi.spyOn(toolConfigsApi, "listToolConfigs").mockResolvedValue(mockConfigs);
|
||||
vi.spyOn(configFoldersApi, "listConfigFolders").mockResolvedValue(mockFolders);
|
||||
|
||||
render(<ToolWorkshopPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("VS Code Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /create tool type/i }));
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/name/i), {
|
||||
target: { value: "docker-tool" },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText(/display name/i), {
|
||||
target: { value: "Docker Tool" },
|
||||
});
|
||||
|
||||
// Switch to dockerfile
|
||||
fireEvent.click(screen.getByLabelText(/dockerfile/i));
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/dockerfile template/i), {
|
||||
target: { value: "FROM python:3.11\\nRUN pip install flask" },
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /create$/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: "docker-tool",
|
||||
definition_type: "dockerfile",
|
||||
dockerfile_template: "FROM python:3.11\\nRUN pip install flask",
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows readiness probe fields", async () => {
|
||||
vi.spyOn(toolTypesApi, "listToolTypes").mockResolvedValue(mockToolTypes);
|
||||
vi.spyOn(toolConfigsApi, "listToolConfigs").mockResolvedValue(mockConfigs);
|
||||
vi.spyOn(configFoldersApi, "listConfigFolders").mockResolvedValue(mockFolders);
|
||||
|
||||
render(<ToolWorkshopPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("VS Code Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /create tool type/i }));
|
||||
|
||||
expect(screen.getByLabelText(/readiness command/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/timeout/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/interval/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens config creation form", async () => {
|
||||
vi.spyOn(toolTypesApi, "listToolTypes").mockResolvedValue(mockToolTypes);
|
||||
vi.spyOn(toolConfigsApi, "listToolConfigs").mockResolvedValue(mockConfigs);
|
||||
vi.spyOn(configFoldersApi, "listConfigFolders").mockResolvedValue(mockFolders);
|
||||
vi.spyOn(toolConfigsApi, "getToolConfigDefaults").mockResolvedValue({
|
||||
tool_type_id: "type-1",
|
||||
suggested_configs: [],
|
||||
port_override: null,
|
||||
});
|
||||
|
||||
render(<ToolWorkshopPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("VS Code Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /configs/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("OPENAI_API_KEY")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /add config/i }));
|
||||
|
||||
expect(screen.getByLabelText(/key/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/value/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("creates config with advanced fields", async () => {
|
||||
const listMock = vi.spyOn(toolTypesApi, "listToolTypes").mockResolvedValue(mockToolTypes);
|
||||
const configsListMock = vi.spyOn(toolConfigsApi, "listToolConfigs").mockResolvedValue(mockConfigs);
|
||||
const createMock = vi.spyOn(toolConfigsApi, "createToolConfig").mockResolvedValue(mockConfigs[1]);
|
||||
vi.spyOn(configFoldersApi, "listConfigFolders").mockResolvedValue(mockFolders);
|
||||
vi.spyOn(toolConfigsApi, "getToolConfigDefaults").mockResolvedValue({
|
||||
tool_type_id: "type-1",
|
||||
suggested_configs: [],
|
||||
port_override: null,
|
||||
});
|
||||
|
||||
render(<ToolWorkshopPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("VS Code Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /configs/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("OPENAI_API_KEY")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /add config/i }));
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/key/i), {
|
||||
target: { value: "MY_CONFIG" },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText(/value/i), {
|
||||
target: { value: "my-value" },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText(/port override/i), {
|
||||
target: { value: "9090" },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText(/start command/i), {
|
||||
target: { value: "python app.py" },
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /create$/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
key: "MY_CONFIG",
|
||||
value: "my-value",
|
||||
port_override: 9090,
|
||||
start_command: "python app.py",
|
||||
})
|
||||
);
|
||||
});
|
||||
expect(configsListMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("opens folder creation form", async () => {
|
||||
vi.spyOn(toolTypesApi, "listToolTypes").mockResolvedValue(mockToolTypes);
|
||||
vi.spyOn(toolConfigsApi, "listToolConfigs").mockResolvedValue(mockConfigs);
|
||||
vi.spyOn(configFoldersApi, "listConfigFolders").mockResolvedValue(mockFolders);
|
||||
|
||||
render(<ToolWorkshopPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("VS Code Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /folders/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("my-dotfiles")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /create folder/i }));
|
||||
|
||||
expect(screen.getByLabelText(/name/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/mount path/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("creates config folder successfully", async () => {
|
||||
const listMock = vi.spyOn(toolTypesApi, "listToolTypes").mockResolvedValue(mockToolTypes);
|
||||
const foldersListMock = vi.spyOn(configFoldersApi, "listConfigFolders").mockResolvedValue(mockFolders);
|
||||
const createMock = vi.spyOn(configFoldersApi, "createConfigFolder").mockResolvedValue(mockFolders[0]);
|
||||
vi.spyOn(toolConfigsApi, "listToolConfigs").mockResolvedValue(mockConfigs);
|
||||
|
||||
render(<ToolWorkshopPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("VS Code Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /folders/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("my-dotfiles")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /create folder/i }));
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/name/i), {
|
||||
target: { value: "new-folder" },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText(/mount path/i), {
|
||||
target: { value: "/home/dev" },
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /create$/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: "new-folder",
|
||||
mount_path: "/home/dev",
|
||||
})
|
||||
);
|
||||
});
|
||||
expect(foldersListMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("shows folder active/inactive status", async () => {
|
||||
vi.spyOn(toolTypesApi, "listToolTypes").mockResolvedValue(mockToolTypes);
|
||||
vi.spyOn(toolConfigsApi, "listToolConfigs").mockResolvedValue(mockConfigs);
|
||||
vi.spyOn(configFoldersApi, "listConfigFolders").mockResolvedValue(mockFolders);
|
||||
|
||||
render(<ToolWorkshopPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("VS Code Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /folders/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("my-dotfiles")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Check that active folder is marked
|
||||
const activeFolder = screen.getByText("my-dotfiles").closest("[data-testid='folder-item']") ||
|
||||
screen.getByText("my-dotfiles").parentElement;
|
||||
expect(activeFolder?.textContent).toContain("active");
|
||||
});
|
||||
|
||||
it("validates tool type before creation", async () => {
|
||||
vi.spyOn(toolTypesApi, "listToolTypes").mockResolvedValue(mockToolTypes);
|
||||
vi.spyOn(toolConfigsApi, "listToolConfigs").mockResolvedValue(mockConfigs);
|
||||
vi.spyOn(configFoldersApi, "listConfigFolders").mockResolvedValue(mockFolders);
|
||||
const validateMock = vi.spyOn(toolTypesApi, "validateToolType").mockResolvedValue({
|
||||
valid: false,
|
||||
errors: ["Invalid YAML syntax"],
|
||||
});
|
||||
|
||||
render(<ToolWorkshopPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("VS Code Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /create tool type/i }));
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/name/i), {
|
||||
target: { value: "bad-tool" },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText(/display name/i), {
|
||||
target: { value: "Bad Tool" },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText(/compose template/i), {
|
||||
target: { value: "invalid: yaml: [" },
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /validate/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(validateMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(screen.getByText(/invalid yaml syntax/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handles error state gracefully", async () => {
|
||||
vi.spyOn(toolTypesApi, "listToolTypes").mockRejectedValue(new Error("Network error"));
|
||||
vi.spyOn(toolConfigsApi, "listToolConfigs").mockRejectedValue(new Error("Network error"));
|
||||
vi.spyOn(configFoldersApi, "listConfigFolders").mockRejectedValue(new Error("Network error"));
|
||||
|
||||
render(<ToolWorkshopPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/failed to load/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByRole("button", { name: /retry/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("retries loading after error", async () => {
|
||||
const listMock = vi.spyOn(toolTypesApi, "listToolTypes")
|
||||
.mockRejectedValueOnce(new Error("Network error"))
|
||||
.mockResolvedValueOnce(mockToolTypes);
|
||||
vi.spyOn(toolConfigsApi, "listToolConfigs")
|
||||
.mockRejectedValueOnce(new Error("Network error"))
|
||||
.mockResolvedValueOnce(mockConfigs);
|
||||
vi.spyOn(configFoldersApi, "listConfigFolders")
|
||||
.mockRejectedValueOnce(new Error("Network error"))
|
||||
.mockResolvedValueOnce(mockFolders);
|
||||
|
||||
render(<ToolWorkshopPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/failed to load/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /retry/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("VS Code Server")).toBeInTheDocument();
|
||||
});
|
||||
expect(listMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("deletes tool type successfully", async () => {
|
||||
const listMock = vi.spyOn(toolTypesApi, "listToolTypes").mockResolvedValue(mockToolTypes);
|
||||
const deleteMock = vi.spyOn(toolTypesApi, "deleteToolType").mockResolvedValue(undefined);
|
||||
vi.spyOn(toolConfigsApi, "listToolConfigs").mockResolvedValue(mockConfigs);
|
||||
vi.spyOn(configFoldersApi, "listConfigFolders").mockResolvedValue(mockFolders);
|
||||
|
||||
render(<ToolWorkshopPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Custom Tool")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Find and click delete button for custom tool (not built-in)
|
||||
const customToolCard = screen.getByText("Custom Tool").closest("[data-testid='tool-type-item']") ||
|
||||
screen.getByText("Custom Tool").parentElement;
|
||||
if (customToolCard) {
|
||||
const deleteButton = within(customToolCard as HTMLElement).queryByRole("button", { name: /delete/i });
|
||||
if (deleteButton) {
|
||||
fireEvent.click(deleteButton);
|
||||
|
||||
// Confirm deletion
|
||||
const confirmButton = screen.queryByRole("button", { name: /confirm/i });
|
||||
if (confirmButton) {
|
||||
fireEvent.click(confirmButton);
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(deleteMock).toHaveBeenCalledWith("type-2");
|
||||
});
|
||||
expect(listMock).toHaveBeenCalledTimes(2);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user