mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-11-05 01:50:36 +00:00
* fix(frontend): make docs search work on desktop and mobile Docs search on deerflow.tech was broken twice over: no build step ever generated the Pagefind index that Nextra's <Search> loads, and the custom docs header that replaces Nextra's navbar never rendered a search box. - `pnpm build` now runs `search:index` (pagefind 1.5.2, pinned) to write public/_pagefind/ (gitignored). - The landing Header and its mobile sheet take an optional `search` slot; only the docs layout fills it, with localized labels and an aria-label. - Each docs page carries a Pagefind `lang` filter and queries are scoped to it, since the root <html lang> is fixed and would mix en/zh results. - globals.css restyles the results panel with app tokens because Nextra's <Head> variables are unset, lifts it above the mobile sheet, and keeps it tappable; the sheet no longer closes before a result navigates and closes after navigation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(frontend): add DESIGN.md describing the shipped design system Records the existing tokens, typography, components, docs (Nextra) overrides, motion, accessibility and copy rules, plus the landing-only decorative exceptions, so UI changes have something to be checked against. Linked from frontend/AGENTS.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(frontend): address Codex review of docs search - Chinese search: the root <html lang> is fixed, so Pagefind indexed zh pages with the English tokenizer and multi-character words (e.g. 偏好) never matched. scripts/build-search-index.mjs now sets each page's lang from its route before indexing, producing separate en-US and zh-CN indexes, and DocsHtmlLang syncs <html lang> at runtime (resetting Pagefind on a language switch) so queries hit the right index. - Mobile menu: reset the open state when the route changes instead of comparing against the saved path, so Back no longer reopens the sheet. - Shortcut: stop mounting a second <Search> in Nextra's unused mobile menu, which captured Cmd/Ctrl+K and focused an offscreen input at 640-767px. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(frontend): keep guidance within size budget * fix(frontend): address docs search review feedback Keep Nextra results within the mobile sheet scroll boundary and dismiss same-hash selections by pointer or keyboard. Rebuild Pagefind output without stale fragments and replace UI source-string checks with behavioral coverage. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
152 lines
5.0 KiB
TypeScript
152 lines
5.0 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, rs } from "@rstest/core";
|
|
import {
|
|
cleanup,
|
|
fireEvent,
|
|
render,
|
|
screen,
|
|
waitFor,
|
|
} from "@testing-library/react";
|
|
import type { Window as HappyWindow } from "happy-dom";
|
|
import { Search } from "nextra/components";
|
|
import type { ComponentProps } from "react";
|
|
|
|
const navigation = rs.hoisted(() => ({
|
|
pathname: "/en/docs",
|
|
push: rs.fn(),
|
|
}));
|
|
|
|
rs.mock("next/navigation", () => ({
|
|
usePathname: () => navigation.pathname,
|
|
useRouter: () => ({ push: navigation.push }),
|
|
}));
|
|
rs.mock("next/link", () => ({
|
|
default: (props: ComponentProps<"a">) => <a {...props} />,
|
|
}));
|
|
|
|
import { MobileNav } from "@/components/landing/mobile-nav";
|
|
|
|
const links = [{ href: "/en/docs", label: "Docs" }];
|
|
|
|
function menu() {
|
|
return (
|
|
<MobileNav links={links} search={<Search aria-label="Search docs" />} />
|
|
);
|
|
}
|
|
|
|
async function openResults() {
|
|
fireEvent.click(screen.getByRole("button", { name: "Open menu" }));
|
|
const input = screen.getByRole("combobox", { name: "Search docs" });
|
|
fireEvent.change(input, { target: { value: "sandbox" } });
|
|
fireEvent.keyDown(input, { key: "ArrowDown" });
|
|
return screen.findByRole("option", { name: /Sandbox heading/ });
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
navigation.pathname = "/en/docs";
|
|
navigation.push.mockReset();
|
|
window.history.replaceState(null, "", "/en/docs#sandbox");
|
|
// Happy DOM queues a hashchange for replaceState; drain setup before mount.
|
|
await (window as unknown as HappyWindow).happyDOM.waitUntilComplete();
|
|
Object.defineProperty(window, "pagefind", {
|
|
configurable: true,
|
|
value: {
|
|
debouncedSearch: async () => ({
|
|
results: [
|
|
{
|
|
data: async () => ({
|
|
url: "/en/docs",
|
|
meta: { title: "Docs" },
|
|
sub_results: [
|
|
{
|
|
url: "/en/docs#sandbox",
|
|
title: "Sandbox heading",
|
|
excerpt: "Run tools in an isolated sandbox.",
|
|
},
|
|
],
|
|
}),
|
|
},
|
|
],
|
|
}),
|
|
},
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
Reflect.deleteProperty(window, "pagefind");
|
|
rs.restoreAllMocks();
|
|
});
|
|
|
|
describe("mobile docs search", () => {
|
|
it("closes on a route change and can reopen after navigating back", async () => {
|
|
const { rerender } = render(menu());
|
|
fireEvent.click(screen.getByRole("button", { name: "Open menu" }));
|
|
expect(screen.getByRole("dialog")).toBeTruthy();
|
|
|
|
navigation.pathname = "/en/docs/harness/sandbox";
|
|
rerender(menu());
|
|
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
|
navigation.pathname = "/en/docs";
|
|
rerender(menu());
|
|
expect(screen.queryByRole("dialog")).toBeNull();
|
|
fireEvent.click(screen.getByRole("button", { name: "Open menu" }));
|
|
expect(screen.getByRole("dialog")).toBeTruthy();
|
|
});
|
|
|
|
it("keeps real Nextra results inside the sheet's allowed scroll region", async () => {
|
|
render(menu());
|
|
const result = await openResults();
|
|
const panel = screen.getByRole("listbox");
|
|
expect(screen.getByRole("dialog").contains(panel)).toBe(true);
|
|
|
|
fireEvent.pointerDown(result, { pointerType: "touch" });
|
|
expect(screen.getByRole("dialog")).toBeTruthy();
|
|
expect(result.isConnected).toBe(true);
|
|
});
|
|
|
|
it("closes after selecting a result at the already selected hash", async () => {
|
|
render(menu());
|
|
const result = await openResults();
|
|
fireEvent.click(result);
|
|
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
|
expect(window.location.hash).toBe("#sandbox");
|
|
});
|
|
|
|
it("closes after keyboard selection at the already selected hash", async () => {
|
|
render(menu());
|
|
await openResults();
|
|
const input = screen.getByRole("combobox", { name: "Search docs" });
|
|
fireEvent.keyDown(input, { key: "ArrowDown" });
|
|
fireEvent.keyDown(input, { key: "Enter" });
|
|
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
|
expect(window.location.hash).toBe("#sandbox");
|
|
});
|
|
|
|
it("closes for same-page keyboard navigation that changes the hash", async () => {
|
|
render(menu());
|
|
await openResults();
|
|
fireEvent(window, new HashChangeEvent("hashchange"));
|
|
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
|
});
|
|
|
|
it("does not close while Enter is composing search text", async () => {
|
|
render(menu());
|
|
await openResults();
|
|
const input = screen.getByRole("combobox", { name: "Search docs" });
|
|
fireEvent.keyDown(input, { key: "ArrowDown" });
|
|
fireEvent.compositionStart(input);
|
|
fireEvent.keyDown(input, { key: "Enter", isComposing: true });
|
|
expect(screen.getByRole("dialog")).toBeTruthy();
|
|
expect((input as HTMLInputElement).value).toBe("sandbox");
|
|
});
|
|
|
|
it("keeps the menu open when a search query has no selected result", () => {
|
|
render(menu());
|
|
fireEvent.click(screen.getByRole("button", { name: "Open menu" }));
|
|
fireEvent.change(screen.getByRole("combobox", { name: "Search docs" }), {
|
|
target: { value: "another query" },
|
|
});
|
|
expect(screen.getByRole("dialog")).toBeTruthy();
|
|
});
|
|
});
|