test(web): add unit tests for MS18 components #503
@@ -0,0 +1,89 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { KnowledgeEditor } from "../KnowledgeEditor";
|
||||
|
||||
// Mock Tiptap since it requires a full DOM with contenteditable support
|
||||
vi.mock("@tiptap/react", () => {
|
||||
const EditorContent = ({ editor }: { editor: unknown }): React.JSX.Element => (
|
||||
<div data-testid="editor-content" data-editor={editor ? "ready" : "null"} />
|
||||
);
|
||||
|
||||
return {
|
||||
useEditor: (): null => null,
|
||||
EditorContent,
|
||||
};
|
||||
});
|
||||
|
||||
// Mock tiptap-markdown
|
||||
vi.mock("tiptap-markdown", () => ({
|
||||
Markdown: {
|
||||
configure: vi.fn().mockReturnValue({}),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock lowlight
|
||||
vi.mock("lowlight", () => ({
|
||||
common: {},
|
||||
createLowlight: vi.fn().mockReturnValue({}),
|
||||
}));
|
||||
|
||||
// Mock extensions
|
||||
vi.mock("@tiptap/starter-kit", () => ({
|
||||
default: { configure: vi.fn().mockReturnValue({}) },
|
||||
}));
|
||||
vi.mock("@tiptap/extension-link", () => ({
|
||||
default: { configure: vi.fn().mockReturnValue({}) },
|
||||
}));
|
||||
vi.mock("@tiptap/extension-table", () => ({
|
||||
Table: { configure: vi.fn().mockReturnValue({}) },
|
||||
}));
|
||||
vi.mock("@tiptap/extension-table-row", () => ({
|
||||
TableRow: {},
|
||||
}));
|
||||
vi.mock("@tiptap/extension-table-cell", () => ({
|
||||
TableCell: {},
|
||||
}));
|
||||
vi.mock("@tiptap/extension-table-header", () => ({
|
||||
TableHeader: {},
|
||||
}));
|
||||
vi.mock("@tiptap/extension-code-block-lowlight", () => ({
|
||||
default: { configure: vi.fn().mockReturnValue({}) },
|
||||
}));
|
||||
vi.mock("@tiptap/extension-placeholder", () => ({
|
||||
default: { configure: vi.fn().mockReturnValue({}) },
|
||||
}));
|
||||
|
||||
describe("KnowledgeEditor", (): void => {
|
||||
const defaultProps = {
|
||||
content: "",
|
||||
onChange: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach((): void => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render loading state when editor is null", (): void => {
|
||||
render(<KnowledgeEditor {...defaultProps} />);
|
||||
expect(screen.getByText("Loading editor...")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render with knowledge-editor class", (): void => {
|
||||
// When editor is null, the loading fallback renders instead
|
||||
const { container } = render(<KnowledgeEditor {...defaultProps} />);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should accept optional placeholder prop", (): void => {
|
||||
// Smoke test that it doesn't crash with custom placeholder
|
||||
render(<KnowledgeEditor {...defaultProps} placeholder="Custom placeholder" />);
|
||||
expect(screen.getByText("Loading editor...")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should accept optional editable prop", (): void => {
|
||||
// Smoke test that it doesn't crash when read-only
|
||||
render(<KnowledgeEditor {...defaultProps} editable={false} />);
|
||||
expect(screen.getByText("Loading editor...")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { WidgetConfigDialog } from "../WidgetConfigDialog";
|
||||
|
||||
describe("WidgetConfigDialog", (): void => {
|
||||
const defaultProps = {
|
||||
widgetId: "TasksWidget-abc123",
|
||||
open: true,
|
||||
onClose: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach((): void => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render nothing when closed", (): void => {
|
||||
const { container } = render(<WidgetConfigDialog {...defaultProps} open={false} />);
|
||||
expect(container.innerHTML).toBe("");
|
||||
});
|
||||
|
||||
it("should render dialog when open", (): void => {
|
||||
render(<WidgetConfigDialog {...defaultProps} />);
|
||||
expect(screen.getByRole("dialog", { name: "Widget Settings" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show widget name in header", (): void => {
|
||||
render(<WidgetConfigDialog {...defaultProps} />);
|
||||
// TasksWidget is registered with displayName "Tasks"
|
||||
expect(screen.getByText(/Tasks.*Settings/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show placeholder message when no config schema", (): void => {
|
||||
render(<WidgetConfigDialog {...defaultProps} />);
|
||||
expect(
|
||||
screen.getByText("No configuration options available for this widget yet.")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onClose when close button is clicked", async (): Promise<void> => {
|
||||
const user = userEvent.setup();
|
||||
render(<WidgetConfigDialog {...defaultProps} />);
|
||||
|
||||
await user.click(screen.getByLabelText("Close"));
|
||||
expect(defaultProps.onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should call onClose when footer Close button is clicked", async (): Promise<void> => {
|
||||
const user = userEvent.setup();
|
||||
render(<WidgetConfigDialog {...defaultProps} />);
|
||||
|
||||
// Footer has a "Close" text button
|
||||
await user.click(screen.getByText("Close"));
|
||||
expect(defaultProps.onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should handle unknown widget type gracefully", (): void => {
|
||||
render(<WidgetConfigDialog {...defaultProps} widgetId="UnknownWidget-xyz" />);
|
||||
// Should show fallback "Widget Settings" when type is not in registry
|
||||
expect(screen.getByText("Widget Settings")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
101
apps/web/src/components/widgets/__tests__/WidgetPicker.test.tsx
Normal file
101
apps/web/src/components/widgets/__tests__/WidgetPicker.test.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { WidgetPicker } from "../WidgetPicker";
|
||||
import type { WidgetPlacement } from "@mosaic/shared";
|
||||
|
||||
describe("WidgetPicker", (): void => {
|
||||
const defaultProps = {
|
||||
open: true,
|
||||
onClose: vi.fn(),
|
||||
onAddWidget: vi.fn(),
|
||||
currentLayout: [] as WidgetPlacement[],
|
||||
};
|
||||
|
||||
beforeEach((): void => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render nothing when closed", (): void => {
|
||||
const { container } = render(<WidgetPicker {...defaultProps} open={false} />);
|
||||
expect(container.innerHTML).toBe("");
|
||||
});
|
||||
|
||||
it("should render dialog when open", (): void => {
|
||||
render(<WidgetPicker {...defaultProps} />);
|
||||
expect(screen.getByRole("dialog", { name: "Add Widget" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display Add Widget heading", (): void => {
|
||||
render(<WidgetPicker {...defaultProps} />);
|
||||
expect(screen.getByText("Add Widget")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render search input", (): void => {
|
||||
render(<WidgetPicker {...defaultProps} />);
|
||||
expect(screen.getByPlaceholderText("Search widgets...")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display available widgets", (): void => {
|
||||
render(<WidgetPicker {...defaultProps} />);
|
||||
// Widget registry has multiple widgets; at least one Add button should appear
|
||||
const addButtons = screen.getAllByText("Add");
|
||||
expect(addButtons.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should filter widgets by search text", async (): Promise<void> => {
|
||||
const user = userEvent.setup();
|
||||
render(<WidgetPicker {...defaultProps} />);
|
||||
|
||||
const searchInput = screen.getByPlaceholderText("Search widgets...");
|
||||
// Type a search term that won't match any widget
|
||||
await user.type(searchInput, "zzz-nonexistent-widget-zzz");
|
||||
|
||||
expect(screen.getByText("No widgets found")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onAddWidget when Add is clicked", async (): Promise<void> => {
|
||||
const user = userEvent.setup();
|
||||
const onAdd = vi.fn();
|
||||
render(<WidgetPicker {...defaultProps} onAddWidget={onAdd} />);
|
||||
|
||||
const addButtons = screen.getAllByText("Add");
|
||||
await user.click(addButtons[0]!);
|
||||
|
||||
expect(onAdd).toHaveBeenCalledTimes(1);
|
||||
const placement = onAdd.mock.calls[0]![0] as WidgetPlacement;
|
||||
expect(placement).toHaveProperty("i");
|
||||
expect(placement).toHaveProperty("x");
|
||||
expect(placement).toHaveProperty("y");
|
||||
expect(placement).toHaveProperty("w");
|
||||
expect(placement).toHaveProperty("h");
|
||||
});
|
||||
|
||||
it("should call onClose when close button is clicked", async (): Promise<void> => {
|
||||
const user = userEvent.setup();
|
||||
render(<WidgetPicker {...defaultProps} />);
|
||||
|
||||
await user.click(screen.getByLabelText("Close"));
|
||||
expect(defaultProps.onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should place new widgets after existing layout items", async (): Promise<void> => {
|
||||
const user = userEvent.setup();
|
||||
const onAdd = vi.fn();
|
||||
const existingLayout: WidgetPlacement[] = [
|
||||
{ i: "test-1", x: 0, y: 0, w: 6, h: 3 },
|
||||
{ i: "test-2", x: 0, y: 3, w: 6, h: 2 },
|
||||
];
|
||||
render(<WidgetPicker {...defaultProps} onAddWidget={onAdd} currentLayout={existingLayout} />);
|
||||
|
||||
const addButtons = screen.getAllByText("Add");
|
||||
await user.click(addButtons[0]!);
|
||||
|
||||
const placement = onAdd.mock.calls[0]![0] as WidgetPlacement;
|
||||
// Should be placed at y=5 (3+2) to avoid overlap
|
||||
expect(placement.y).toBe(5);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user