refactor(search): extract search into workspace package

This commit is contained in:
Maofeng
2026-09-20 15:52:09 +08:00
parent e02c3dceb5
commit 1fc56b6982
34 changed files with 262 additions and 360 deletions
+140
View File
@@ -0,0 +1,140 @@
// @vitest-environment jsdom
import {
cleanup,
fireEvent,
render,
screen,
waitFor,
} from "@testing-library/react"
import { I18nProvider } from "@workspace/i18n"
import { afterEach, describe, expect, it, vi } from "vitest"
import { SearchProvider } from "./context"
import { SearchDialog } from "./dialog"
import { searchDialogHandle } from "./handle"
import { splitHighlightSegments } from "./highlight"
import { messages as englishMessages } from "./locales/en-US"
import type { SearchAdapter } from "./types"
class ResizeObserverStub {
disconnect() {}
observe() {}
unobserve() {}
}
vi.stubGlobal("ResizeObserver", ResizeObserverStub)
Object.defineProperty(Element.prototype, "getAnimations", {
configurable: true,
value: () => [],
})
function renderSearch({
adapter,
onSelect,
}: {
adapter: SearchAdapter
onSelect?: (event: { item: { id: string }; query: string }) => void
}) {
return render(
<I18nProvider locale="en-US" catalogs={{ "en-US": englishMessages }}>
<SearchProvider
adapter={adapter}
historyStorageKey={false}
onSelect={onSelect}
>
<SearchDialog hotkey={false} />
</SearchProvider>
</I18nProvider>
)
}
describe("search package", () => {
afterEach(() => {
cleanup()
window.localStorage.clear()
})
it("delegates searching, pagination, and selection to the provider", async () => {
const adapter: SearchAdapter = {
search: vi.fn(async ({ cursor, query }) => {
if (cursor) {
return {
groups: [
{
id: "people",
label: "People",
items: [{ id: "2", title: "Grace Hopper" }],
},
],
nextCursor: null,
}
}
return {
groups: [
{
id: "people",
label: "People",
total: 2,
items: [
{
id: "1",
title: "Ada Lovelace",
description: `Matched ${query}`,
},
],
},
],
nextCursor: "next-page",
}
}),
}
const onSelect = vi.fn()
renderSearch({ adapter, onSelect })
searchDialogHandle.open(null)
const input = await screen.findByPlaceholderText("Search your workspace…")
fireEvent.change(input, { target: { value: "ada" } })
await waitFor(() =>
expect(adapter.search).toHaveBeenCalledWith(
expect.objectContaining({ query: "ada" })
)
)
await waitFor(() =>
expect(screen.getByRole("dialog").textContent).toContain("Ada Lovelace")
)
fireEvent.click(screen.getByRole("button", { name: "Load more" }))
await waitFor(() =>
expect(screen.getByRole("dialog").textContent).toContain("Grace Hopper")
)
expect(adapter.search).toHaveBeenLastCalledWith(
expect.objectContaining({ cursor: "next-page", query: "ada" })
)
fireEvent.click(screen.getByRole("option", { name: "Open Ada Lovelace" }))
await waitFor(() =>
expect(onSelect).toHaveBeenCalledWith(
expect.objectContaining({
item: expect.objectContaining({ id: "1" }),
query: "ada",
})
)
)
})
it("splits case-insensitive literal query matches for highlighting", () => {
expect(splitHighlightSegments("Ada ADA Lovelace", "ada")).toEqual([
{ highlighted: true, text: "Ada" },
{ highlighted: false, text: " " },
{ highlighted: true, text: "ADA" },
{ highlighted: false, text: " Lovelace" },
])
expect(splitHighlightSegments("price [usd]", "[usd]")).toEqual([
{ highlighted: false, text: "price " },
{ highlighted: true, text: "[usd]" },
])
})
})