diff --git a/apps/web/src/components/knowledge/__tests__/KnowledgeEditor.test.tsx b/apps/web/src/components/knowledge/__tests__/KnowledgeEditor.test.tsx
new file mode 100644
index 0000000..14821dc
--- /dev/null
+++ b/apps/web/src/components/knowledge/__tests__/KnowledgeEditor.test.tsx
@@ -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 => (
+
+ );
+
+ 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();
+ 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();
+ expect(container.firstChild).toBeInTheDocument();
+ });
+
+ it("should accept optional placeholder prop", (): void => {
+ // Smoke test that it doesn't crash with custom placeholder
+ render();
+ expect(screen.getByText("Loading editor...")).toBeInTheDocument();
+ });
+
+ it("should accept optional editable prop", (): void => {
+ // Smoke test that it doesn't crash when read-only
+ render();
+ expect(screen.getByText("Loading editor...")).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/src/components/widgets/__tests__/WidgetConfigDialog.test.tsx b/apps/web/src/components/widgets/__tests__/WidgetConfigDialog.test.tsx
new file mode 100644
index 0000000..e07d9cd
--- /dev/null
+++ b/apps/web/src/components/widgets/__tests__/WidgetConfigDialog.test.tsx
@@ -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();
+ expect(container.innerHTML).toBe("");
+ });
+
+ it("should render dialog when open", (): void => {
+ render();
+ expect(screen.getByRole("dialog", { name: "Widget Settings" })).toBeInTheDocument();
+ });
+
+ it("should show widget name in header", (): void => {
+ render();
+ // TasksWidget is registered with displayName "Tasks"
+ expect(screen.getByText(/Tasks.*Settings/)).toBeInTheDocument();
+ });
+
+ it("should show placeholder message when no config schema", (): void => {
+ render();
+ expect(
+ screen.getByText("No configuration options available for this widget yet.")
+ ).toBeInTheDocument();
+ });
+
+ it("should call onClose when close button is clicked", async (): Promise => {
+ const user = userEvent.setup();
+ render();
+
+ await user.click(screen.getByLabelText("Close"));
+ expect(defaultProps.onClose).toHaveBeenCalledTimes(1);
+ });
+
+ it("should call onClose when footer Close button is clicked", async (): Promise => {
+ const user = userEvent.setup();
+ render();
+
+ // 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();
+ // Should show fallback "Widget Settings" when type is not in registry
+ expect(screen.getByText("Widget Settings")).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/src/components/widgets/__tests__/WidgetPicker.test.tsx b/apps/web/src/components/widgets/__tests__/WidgetPicker.test.tsx
new file mode 100644
index 0000000..dfc680a
--- /dev/null
+++ b/apps/web/src/components/widgets/__tests__/WidgetPicker.test.tsx
@@ -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();
+ expect(container.innerHTML).toBe("");
+ });
+
+ it("should render dialog when open", (): void => {
+ render();
+ expect(screen.getByRole("dialog", { name: "Add Widget" })).toBeInTheDocument();
+ });
+
+ it("should display Add Widget heading", (): void => {
+ render();
+ expect(screen.getByText("Add Widget")).toBeInTheDocument();
+ });
+
+ it("should render search input", (): void => {
+ render();
+ expect(screen.getByPlaceholderText("Search widgets...")).toBeInTheDocument();
+ });
+
+ it("should display available widgets", (): void => {
+ render();
+ // 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 => {
+ const user = userEvent.setup();
+ render();
+
+ 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 => {
+ const user = userEvent.setup();
+ const onAdd = vi.fn();
+ render();
+
+ 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 => {
+ const user = userEvent.setup();
+ render();
+
+ await user.click(screen.getByLabelText("Close"));
+ expect(defaultProps.onClose).toHaveBeenCalledTimes(1);
+ });
+
+ it("should place new widgets after existing layout items", async (): Promise => {
+ 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();
+
+ 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);
+ });
+});