Skip to content

Commit 0a2f762

Browse files
committed
test(components): add per-section tests for SettingsTab extractions
Add focused unit tests for ModelSection, ContextWindowSection, SemanticSearchSection, KbSection, AdvancedSearchSection, VariablesSection, and JiraSection. Each test mocks only the props that section needs. The omnibus SettingsTab.test.tsx remains as the integration suite exercising cross-section flows.
1 parent a2076d4 commit 0a2f762

7 files changed

Lines changed: 403 additions & 0 deletions
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// @vitest-environment jsdom
2+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
3+
import { afterEach, describe, expect, it, vi } from "vitest";
4+
import { AdvancedSearchSection } from "./AdvancedSearchSection";
5+
6+
describe("AdvancedSearchSection", () => {
7+
afterEach(() => cleanup());
8+
9+
it("renders the vector toggle reflecting the current enabled state", () => {
10+
render(
11+
<AdvancedSearchSection vectorEnabled={true} onVectorToggle={vi.fn()} />,
12+
);
13+
14+
const toggle = screen.getByLabelText(
15+
"Enable vector embeddings",
16+
) as HTMLInputElement;
17+
expect(toggle.checked).toBe(true);
18+
});
19+
20+
it("calls onVectorToggle when the checkbox flips", () => {
21+
const onVectorToggle = vi.fn();
22+
render(
23+
<AdvancedSearchSection
24+
vectorEnabled={false}
25+
onVectorToggle={onVectorToggle}
26+
/>,
27+
);
28+
29+
fireEvent.click(screen.getByLabelText("Enable vector embeddings"));
30+
expect(onVectorToggle).toHaveBeenCalledTimes(1);
31+
});
32+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// @vitest-environment jsdom
2+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
3+
import { afterEach, describe, expect, it, vi } from "vitest";
4+
import { ContextWindowSection } from "./ContextWindowSection";
5+
6+
describe("ContextWindowSection", () => {
7+
afterEach(() => cleanup());
8+
9+
it("disables the select and shows the note when no model is loaded", () => {
10+
render(
11+
<ContextWindowSection
12+
loadedModel={null}
13+
contextWindowSize={null}
14+
onContextWindowChange={vi.fn()}
15+
/>,
16+
);
17+
18+
const select = screen.getByLabelText(
19+
"Context window size",
20+
) as HTMLSelectElement;
21+
expect(select.disabled).toBe(true);
22+
expect(
23+
screen.getByText("Load a model to configure context window."),
24+
).toBeTruthy();
25+
});
26+
27+
it("invokes onContextWindowChange with the new value when a size is selected", () => {
28+
const onContextWindowChange = vi.fn();
29+
render(
30+
<ContextWindowSection
31+
loadedModel="llama-3.1-8b-instruct"
32+
contextWindowSize={4096}
33+
onContextWindowChange={onContextWindowChange}
34+
/>,
35+
);
36+
37+
fireEvent.change(screen.getByLabelText("Context window size"), {
38+
target: { value: "8192" },
39+
});
40+
expect(onContextWindowChange).toHaveBeenCalledWith("8192");
41+
});
42+
});
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// @vitest-environment jsdom
2+
import {
3+
cleanup,
4+
fireEvent,
5+
render,
6+
screen,
7+
waitFor,
8+
} from "@testing-library/react";
9+
import { afterEach, describe, expect, it, vi } from "vitest";
10+
import { JiraSection } from "./JiraSection";
11+
12+
describe("JiraSection", () => {
13+
afterEach(() => cleanup());
14+
15+
it("submits the form with the entered credentials when not configured", async () => {
16+
const onJiraConnect = vi.fn().mockResolvedValue(undefined);
17+
render(
18+
<JiraSection
19+
jiraConfigured={false}
20+
jiraConfig={null}
21+
jiraLoading={false}
22+
onJiraConnect={onJiraConnect}
23+
onJiraDisconnect={vi.fn()}
24+
/>,
25+
);
26+
27+
fireEvent.change(screen.getByLabelText("Jira URL"), {
28+
target: { value: "https://example.atlassian.net" },
29+
});
30+
fireEvent.change(screen.getByLabelText("Email"), {
31+
target: { value: "dev@example.com" },
32+
});
33+
fireEvent.change(screen.getByLabelText("API Token"), {
34+
target: { value: "secret-token" },
35+
});
36+
fireEvent.click(screen.getByRole("button", { name: "Connect" }));
37+
38+
await waitFor(() =>
39+
expect(onJiraConnect).toHaveBeenCalledWith(
40+
"https://example.atlassian.net",
41+
"dev@example.com",
42+
"secret-token",
43+
),
44+
);
45+
});
46+
47+
it("renders the connected state and triggers disconnect when Disconnect is clicked", () => {
48+
const onJiraDisconnect = vi.fn();
49+
render(
50+
<JiraSection
51+
jiraConfigured={true}
52+
jiraConfig={{
53+
base_url: "https://example.atlassian.net",
54+
email: "dev@example.com",
55+
}}
56+
jiraLoading={false}
57+
onJiraConnect={vi.fn()}
58+
onJiraDisconnect={onJiraDisconnect}
59+
/>,
60+
);
61+
62+
expect(
63+
screen.getByText("Connected to https://example.atlassian.net"),
64+
).toBeTruthy();
65+
expect(screen.getByText("Account: dev@example.com")).toBeTruthy();
66+
fireEvent.click(screen.getByRole("button", { name: "Disconnect" }));
67+
expect(onJiraDisconnect).toHaveBeenCalledTimes(1);
68+
});
69+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// @vitest-environment jsdom
2+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
3+
import { afterEach, describe, expect, it, vi } from "vitest";
4+
import { KbSection } from "./KbSection";
5+
6+
describe("KbSection", () => {
7+
afterEach(() => cleanup());
8+
9+
it("renders the empty placeholder and Select Folder button when no folder is set", () => {
10+
const onSelectKbFolder = vi.fn();
11+
render(
12+
<KbSection
13+
kbFolder={null}
14+
indexStats={null}
15+
loading={null}
16+
onSelectKbFolder={onSelectKbFolder}
17+
onRebuildIndex={vi.fn()}
18+
/>,
19+
);
20+
21+
expect(screen.getByText("No folder selected")).toBeTruthy();
22+
fireEvent.click(screen.getByRole("button", { name: "Select Folder" }));
23+
expect(onSelectKbFolder).toHaveBeenCalledTimes(1);
24+
});
25+
26+
it("shows index stats and triggers rebuild when a folder is configured", () => {
27+
const onRebuildIndex = vi.fn();
28+
render(
29+
<KbSection
30+
kbFolder="/tmp/kb"
31+
indexStats={{ total_chunks: 42, total_files: 7 }}
32+
loading={null}
33+
onSelectKbFolder={vi.fn()}
34+
onRebuildIndex={onRebuildIndex}
35+
/>,
36+
);
37+
38+
expect(screen.getByText("/tmp/kb")).toBeTruthy();
39+
expect(screen.getByText("7")).toBeTruthy();
40+
expect(screen.getByText("42")).toBeTruthy();
41+
42+
fireEvent.click(screen.getByRole("button", { name: "Rebuild Index" }));
43+
expect(onRebuildIndex).toHaveBeenCalledTimes(1);
44+
});
45+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// @vitest-environment jsdom
2+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
3+
import { afterEach, describe, expect, it, vi } from "vitest";
4+
import { ModelSection } from "./ModelSection";
5+
6+
function renderSection(
7+
overrides: Partial<Parameters<typeof ModelSection>[0]> = {},
8+
) {
9+
const defaults = {
10+
loadedModel: null as string | null,
11+
loadedModelInfo: null,
12+
downloadedModels: [] as string[],
13+
isEmbeddingLoaded: false,
14+
searchApiEmbeddingStatus: null,
15+
kbFolder: null as string | null,
16+
memoryKernelPreflight: null,
17+
memoryKernelLoading: false,
18+
allowUnverifiedLocalModels: false,
19+
loading: null as string | null,
20+
isDownloading: false,
21+
downloadProgress: null,
22+
onLoadModel: vi.fn(),
23+
onUnloadModel: vi.fn(),
24+
onDownloadModel: vi.fn(),
25+
onCancelDownload: vi.fn(),
26+
onLoadCustomModel: vi.fn(),
27+
onAllowUnverifiedLocalModelsChange: vi.fn(),
28+
onRefreshMemoryKernel: vi.fn(),
29+
};
30+
const props = { ...defaults, ...overrides };
31+
return { props, ...render(<ModelSection {...props} />) };
32+
}
33+
34+
describe("ModelSection", () => {
35+
afterEach(() => cleanup());
36+
37+
it("shows the Download button for an undownloaded recommended model", () => {
38+
const { props } = renderSection();
39+
const downloadButton = screen.getAllByRole("button", {
40+
name: "Download",
41+
})[0];
42+
fireEvent.click(downloadButton);
43+
expect(props.onDownloadModel).toHaveBeenCalledWith("llama-3.1-8b-instruct");
44+
});
45+
46+
it("toggles the Other Supported Models list", () => {
47+
renderSection();
48+
fireEvent.click(
49+
screen.getByRole("button", { name: "Show other supported models" }),
50+
);
51+
expect(screen.getByText("Llama 3.2 1B Instruct")).toBeTruthy();
52+
fireEvent.click(
53+
screen.getByRole("button", { name: "Hide other supported models" }),
54+
);
55+
});
56+
57+
it("triggers onAllowUnverifiedLocalModelsChange when the toggle flips", () => {
58+
const { props } = renderSection();
59+
fireEvent.click(
60+
screen.getByLabelText("Allow unverified local models (advanced)"),
61+
);
62+
expect(props.onAllowUnverifiedLocalModelsChange).toHaveBeenCalledWith(true);
63+
});
64+
});
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// @vitest-environment jsdom
2+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
3+
import { afterEach, describe, expect, it, vi } from "vitest";
4+
import { SemanticSearchSection } from "./SemanticSearchSection";
5+
6+
function renderSection(
7+
overrides: Partial<Parameters<typeof SemanticSearchSection>[0]> = {},
8+
) {
9+
const defaults = {
10+
embeddingDownloaded: false,
11+
isEmbeddingLoaded: false,
12+
embeddingLoading: false,
13+
embeddingModelInfo: null,
14+
vectorEnabled: false,
15+
generatingEmbeddings: false,
16+
isDownloading: false,
17+
downloadProgress: null,
18+
searchApiEmbeddingStatus: {
19+
installed: false,
20+
ready: false,
21+
model_name: "sentence-transformers/all-MiniLM-L6-v2",
22+
revision: "pinned",
23+
local_path: null,
24+
error: null,
25+
},
26+
searchApiEmbeddingLoading: false,
27+
searchApiEmbeddingBadge: {
28+
label: "Not Installed",
29+
className: "not-downloaded",
30+
detail: "Install this managed model...",
31+
},
32+
onCancelDownload: vi.fn(),
33+
onDownloadEmbeddingModel: vi.fn(),
34+
onLoadEmbeddingModel: vi.fn(),
35+
onUnloadEmbeddingModel: vi.fn(),
36+
onGenerateEmbeddings: vi.fn(),
37+
onInstallSearchApiEmbeddingModel: vi.fn(),
38+
onRefreshSearchApiEmbeddingStatus: vi.fn(),
39+
};
40+
const props = { ...defaults, ...overrides };
41+
return { props, ...render(<SemanticSearchSection {...props} />) };
42+
}
43+
44+
describe("SemanticSearchSection", () => {
45+
afterEach(() => cleanup());
46+
47+
it("prompts to download the desktop model when not downloaded", () => {
48+
const { props } = renderSection();
49+
fireEvent.click(screen.getByRole("button", { name: "Download Model" }));
50+
expect(props.onDownloadEmbeddingModel).toHaveBeenCalledTimes(1);
51+
});
52+
53+
it("renders the Unload control when loaded and triggers onUnloadEmbeddingModel", () => {
54+
const { props } = renderSection({
55+
embeddingDownloaded: true,
56+
isEmbeddingLoaded: true,
57+
embeddingModelInfo: { name: "nomic-embed-text", embedding_dim: 768 },
58+
});
59+
fireEvent.click(screen.getByRole("button", { name: "Unload" }));
60+
expect(props.onUnloadEmbeddingModel).toHaveBeenCalledTimes(1);
61+
});
62+
63+
it("invokes search API install and refresh callbacks", () => {
64+
const { props } = renderSection();
65+
fireEvent.click(screen.getByRole("button", { name: "Install Model" }));
66+
expect(props.onInstallSearchApiEmbeddingModel).toHaveBeenCalledTimes(1);
67+
fireEvent.click(screen.getByRole("button", { name: "Refresh Status" }));
68+
expect(props.onRefreshSearchApiEmbeddingStatus).toHaveBeenCalledTimes(1);
69+
});
70+
});

0 commit comments

Comments
 (0)