diff --git a/packages/website/e2e/plugins.spec.ts b/packages/website/e2e/plugins.spec.ts index 1efc20115c..2793792845 100644 --- a/packages/website/e2e/plugins.spec.ts +++ b/packages/website/e2e/plugins.spec.ts @@ -113,6 +113,35 @@ test("keeps the directory's ranking window when searching", async ({ page }) => ); }); +test("searches for a term typed before the page finished loading", async ({ page }) => { + const loadScripts = await holdScripts(page); + await page.goto("/plugins?window=month", { waitUntil: "domcontentloaded" }); + await searchPlugins(page, "graphite"); + await loadScripts(); + await expect(page.getByRole("button", { name: "Clear search" })).toBeVisible(); + await page.keyboard.press("Enter"); + await expect(page).toHaveURL(/\/plugins\/all\?q=graphite&window=month$/); +}); + +test("filters browse results for a term typed before the page finished loading", async ({ + page, +}) => { + await page.goto("/"); + const loadScripts = await holdScripts(page); + await page.goto("/plugins/all", { waitUntil: "domcontentloaded" }); + const entries = await historyLength(page); + await searchPlugins(page, "gra"); + await loadScripts(); + await expect(page).toHaveURL(/\/plugins\/all\?q=gra$/); + await expect(page.getByRole("heading", { level: 1, name: /^Results for “gra”/ })).toBeVisible(); + await expect(page.getByRole("link", { name: /Dracula/ })).toHaveCount(0); + expect(await historyLength(page)).toBe(entries + 1); + + await page.goBack(); + await expect(page).toHaveURL(/\/plugins\/all$/); + await expect(page.getByRole("heading", { level: 1, name: /^All plugins/ })).toBeVisible(); +}); + test("clears the search with the clear button", async ({ page }) => { await page.goto("/plugins/all"); const searchbox = page.getByRole("searchbox", { name: "Search plugins" }); @@ -304,6 +333,20 @@ test.describe("search engine visits without JavaScript", () => { }); }); +/** Holds the page's scripts so typing lands before hydration; the returned function loads them. */ +async function holdScripts(page: Page): Promise<() => Promise> { + let release!: () => void; + const held = new Promise((resolve) => (release = resolve)); + await page.route(/\.js($|\?)/, async (route) => { + await held; + await route.continue(); + }); + return async () => { + release(); + await page.waitForLoadState("load"); + }; +} + async function searchPlugins(page: Page, term: string) { await page.getByRole("searchbox", { name: "Search plugins" }).fill(term); } diff --git a/packages/website/src/plugins/plugin-search.tsx b/packages/website/src/plugins/plugin-search.tsx index fdd3a4b5f5..ab648bbd58 100644 --- a/packages/website/src/plugins/plugin-search.tsx +++ b/packages/website/src/plugins/plugin-search.tsx @@ -46,6 +46,12 @@ export function PluginSearch({ }, [live, show, term], ); + // A term typed before the page hydrated is in the box but not in state; adopt it. Once hydrated, + // the box always matches state, so this does nothing. + useEffect(() => { + const typed = input.current?.value ?? ""; + if (typed !== term) edit(typed); + }, [edit, term]); const handleChange = useCallback( (event: ChangeEvent) => edit(event.target.value), [edit],