From 71439e51a555bfca21dc21e74bcfb39f1eb5e760 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Sun, 8 Nov 2026 00:27:13 +0000 Subject: [PATCH] fix(website): submit plugin search text during hydration (#9059) Read the submitted form value while hydration state is still catching up with text entered before its input listener. Cover the missed input event without relying on startup timing. Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- packages/website/e2e/plugins.spec.ts | 32 +++++++++++++++++++ .../website/src/plugins/plugin-search.tsx | 10 ++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/packages/website/e2e/plugins.spec.ts b/packages/website/e2e/plugins.spec.ts index 27d4a55c4a..f7d402dd7c 100644 --- a/packages/website/e2e/plugins.spec.ts +++ b/packages/website/e2e/plugins.spec.ts @@ -181,6 +181,23 @@ test("keeps the directory's ranking window when searching", async ({ page }) => ); }); +test("submits the visible search term when its input event was missed", async ({ page }) => { + await page.goto("/plugins?window=month"); + await expect(page.getByRole("button", { name: "Language: English" }).first()).toBeEnabled(); + await fillSearchWithMissedInputEvent(page, "graphite"); + + await page.keyboard.press("Enter"); + + await expect(page).toHaveURL(/\/plugins\/all\?q=graphite&window=month$/); + await expect( + page.getByRole("heading", { level: 1, name: /^Results for “graphite”/ }), + ).toBeVisible(); + await expect(page.getByRole("link", { name: "This month" })).toHaveAttribute( + "aria-current", + "true", + ); +}); + 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" }); @@ -419,6 +436,21 @@ async function searchPlugins(page: Page, term: string) { await page.getByRole("searchbox", { name: "Search plugins" }).fill(term); } +/** Reproduce text entered before hydration's input listener can observe it, without a clock race. */ +async function fillSearchWithMissedInputEvent(page: Page, term: string) { + const search = page.getByRole("searchbox", { name: "Search plugins" }); + await search.evaluate(() => { + for (const eventType of ["input", "change"]) { + document.addEventListener(eventType, (event) => event.stopImmediatePropagation(), { + capture: true, + once: true, + }); + } + }); + await search.fill(term); + await expect(search).toHaveValue(term); +} + async function submitSearch(page: Page, term: string) { await searchPlugins(page, term); await page.keyboard.press("Enter"); diff --git a/packages/website/src/plugins/plugin-search.tsx b/packages/website/src/plugins/plugin-search.tsx index 9279549918..da37d073aa 100644 --- a/packages/website/src/plugins/plugin-search.tsx +++ b/packages/website/src/plugins/plugin-search.tsx @@ -68,12 +68,16 @@ export function PluginSearch({ input.current?.focus(); }, [edit]); const handleSubmit = useCallback( - (event: FormEvent) => { + (event: FormEvent) => { event.preventDefault(); // Live results already show the term. - if (!live) show(term, { replace: false }); + if (!live) { + // Hydration can attach this handler before state adopts text already in the input. + const submittedTerm = String(new FormData(event.currentTarget).get("q") ?? ""); + show(submittedTerm, { replace: false }); + } }, - [live, show, term], + [live, show], ); return (