deer-flow/frontend/tests/unit/components/workspace/knowledge-scope-selector.dom.test.tsx
zhangwei-way b6503e9a35
feat(knowledge): add per-message RAGFlow retrieval scope (#5238)
* feat(knowledge): integrate RAGFlow retrieval and management

* test(knowledge): cover merged listing tool

* feat(knowledge): add per-message retrieval scope

* chore(docs): remove unrelated document

* docs(knowledge): add interaction screenshots

* feat(knowledge): simplify scope selector trigger

* docs(knowledge): refresh selector screenshot

* feat(knowledge): defer standalone management

* docs(knowledge): show chat-only scope UI

* fix(knowledge): honor scope on clarification replies

* fix(knowledge): harden scoped replay validation

* docs(knowledge): clarify replay scope precedence

* fix(knowledge): keep provider settings on tools

* fix(config): preserve tools-only knowledge settings

* fix(knowledge): submit custom assistant identity

* refactor(knowledge): trim PR scope changes

* fix(knowledge): sanitize document scope display

* feat(knowledge): enable scope selection in main chat

* fix(knowledge): emphasize active scope icon without button frame

* fix(knowledge): close context scrubbing and refresh e2e checks

* fix(knowledge): preserve idempotent canonical retries

* fix(knowledge): accept promptless conversation runs

* style(knowledge): format backend regression tests

* chore(knowledge): trim PR scope and fix frontend format

* fix(knowledge): remove shared-scope notice

* fix(knowledge): remove scope persistence notice

* docs(knowledge): include main chat in catalog scope

* fix(knowledge): preserve scope recovery and upgrades

* fix(config): preserve LightRAG knowledge upgrades

---------

Co-authored-by: foreleven <for-eleven@hotmail.com>
2026-09-18 16:59:31 +08:00

122 lines
3.7 KiB
TypeScript

import { afterEach, describe, expect, it, rs } from "@rstest/core";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
act,
cleanup,
fireEvent,
render,
screen,
waitFor,
} from "@testing-library/react";
import type { PropsWithChildren } from "react";
import { KnowledgeScopeSelector } from "@/components/workspace/knowledge-scope-selector";
import { I18nProvider } from "@/core/i18n/context";
import type { KnowledgeScopeSelection } from "@/core/knowledge";
afterEach(() => {
cleanup();
rs.restoreAllMocks();
});
function renderSelector(selection: KnowledgeScopeSelection) {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
function Wrapper({ children }: PropsWithChildren) {
return (
<QueryClientProvider client={queryClient}>
<I18nProvider initialLocale="en-US">{children}</I18nProvider>
</QueryClientProvider>
);
}
return render(
<KnowledgeScopeSelector
agentName="researcher"
selection={selection}
onChange={() => undefined}
/>,
{ wrapper: Wrapper },
);
}
function requestUrl(input: RequestInfo | URL): string {
if (typeof input === "string") return input;
if (input instanceof URL) return input.href;
return input.url;
}
describe("KnowledgeScopeSelector trigger", () => {
it("renders only the icon and stays highlighted while retrieval is active", () => {
renderSelector({ mode: "all" });
const trigger = screen.getByRole("button", { name: "Knowledge · All" });
expect(trigger.textContent).toBe("");
expect(trigger.getAttribute("aria-pressed")).toBe("true");
expect(trigger.className).toContain("text-foreground");
expect(trigger.className).not.toContain("bg-primary/10");
expect(trigger.className).not.toContain("border-primary/20");
expect(trigger.querySelector("svg")).not.toBeNull();
});
it("returns to the neutral icon state when retrieval is off", () => {
renderSelector({ mode: "disabled" });
const trigger = screen.getByRole("button", { name: "Knowledge · Off" });
expect(trigger.textContent).toBe("");
expect(trigger.getAttribute("aria-pressed")).toBe("false");
expect(trigger.className).not.toContain("bg-primary/10");
expect(trigger.querySelector("svg")).not.toBeNull();
});
it("does not load documents while an expanded dataset still uses all files", async () => {
const fetch = rs.spyOn(globalThis, "fetch").mockImplementation((input) => {
const url = requestUrl(input);
return Promise.resolve(
Response.json({
items: url.includes("/documents?")
? [{ id: "document-1", name: "Guide", selectable: true }]
: [{ id: "dataset-1", name: "Policies", selectable: true }],
page: 1,
page_size: 100,
total: 1,
}),
);
});
renderSelector({
mode: "selected",
datasets: [
{
id: "dataset-1",
name: "Policies",
documents: { mode: "all" },
},
],
});
fireEvent.click(screen.getByRole("button", { name: "Knowledge · 1 base" }));
await screen.findByText("Policies");
fireEvent.click(screen.getByRole("button", { name: "Files" }));
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
expect(
fetch.mock.calls.some(([input]) =>
requestUrl(input).includes("/documents?"),
),
).toBe(false);
fireEvent.click(screen.getByRole("radio", { name: "Selected files" }));
await waitFor(() => {
expect(
fetch.mock.calls.some(([input]) =>
requestUrl(input).includes("/documents?"),
),
).toBe(true);
});
});
});