Merge remote-tracking branch 'origin/main' into plan-plugin-api-phase7-composers-by-ref

This commit is contained in:
Mohamed Boudra
2026-11-08 13:34:30 +01:00
184 changed files with 11730 additions and 2777 deletions
+30
View File
@@ -9,6 +9,23 @@ async function chooseLanguage(page: Page, language: string) {
await page.getByRole("menuitemradio", { name: language }).click();
}
async function withPageScriptsHeld(page: Page, run: (release: () => void) => Promise<void>) {
let releaseScripts!: () => void;
const scriptsReleased = new Promise<void>((resolve) => {
releaseScripts = resolve;
});
await page.route("**/*.js", async (route) => {
await scriptsReleased;
await route.continue();
});
try {
await run(releaseScripts);
} finally {
releaseScripts();
await page.unrouteAll({ behavior: "wait" });
}
}
function describeAlternates(links: Element[]): string[] {
return links.map((link) => `${link.getAttribute("hreflang")} ${link.getAttribute("href")}`);
}
@@ -114,6 +131,7 @@ test("shows the language as an abbreviation and lists every language by name", a
test("switches language from the keyboard", async ({ page }) => {
await page.goto("/");
await expect(languageButton(page)).toBeEnabled();
await languageButton(page).focus();
await page.keyboard.press("Enter");
await expect(page.getByRole("menuitemradio", { name: "English" })).toBeFocused();
@@ -183,3 +201,15 @@ test("shows the changelog in Chinese", async ({ page }) => {
await expect(page.getByRole("heading", { name: "新增" }).first()).toBeVisible();
await expect(page.getByRole("note")).toHaveCount(0);
});
test("waits for scripts before enabling the language menu", async ({ page }) => {
await withPageScriptsHeld(page, async (releaseScripts) => {
await page.goto("/docs/cli#running-agents", { waitUntil: "commit" });
await expect(languageButton(page)).toBeVisible();
await expect(languageButton(page)).toBeDisabled();
releaseScripts();
await languageButton(page).click();
await expect(page.getByRole("menuitemradio", { name: "English" })).toBeVisible();
});
});
+32
View File
@@ -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");
@@ -1,4 +1,4 @@
import { useLocation, useNavigate } from "@tanstack/react-router";
import { useHydrated, useLocation, useNavigate } from "@tanstack/react-router";
import { AnimatePresence, motion, type Transition } from "framer-motion";
import { Check, ChevronDown, Languages } from "lucide-react";
import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
@@ -33,6 +33,7 @@ type OpenFocus = "current" | "first" | "last";
* the menu button pattern: arrows move between languages, Escape and outside presses close it.
*/
export function LanguageSelector() {
const hydrated = useHydrated();
const { t } = useTranslation();
const locale = useLocale();
const location = useLocation();
@@ -125,6 +126,7 @@ export function LanguageSelector() {
<button
ref={triggerRef}
type="button"
disabled={!hydrated}
aria-label={t("languageSelector.current", { language: LANGUAGE_NAMES[locale] })}
aria-haspopup="menu"
aria-expanded={open}
@@ -68,12 +68,16 @@ export function PluginSearch({
input.current?.focus();
}, [edit]);
const handleSubmit = useCallback(
(event: FormEvent) => {
(event: FormEvent<HTMLFormElement>) => {
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 (
<form