fix(plugins): a Combobox shows a failed refresh instead of the old results

The query keeps its cached data when a reopened list fails to refresh, so the old items stayed
selectable and the error never showed. A failed search now clears the list so its error shows.
The migration guide maps addNewWorkspaceItem, which its registration-coverage test requires, and
the superseded-search test waits on a later search instead of sleeping.
This commit is contained in:
Mohamed Boudra
2026-11-02 09:41:47 +01:00
parent 67a358128b
commit 4706165690
4 changed files with 29 additions and 4 deletions
@@ -21,6 +21,7 @@ interface Issue extends ComboboxItem {
const ONE: Issue = { id: "1", label: "Slow sidebar", branch: "one" };
const TWO: Issue = { id: "2", label: "Crash on resume", branch: "two" };
const THREE: Issue = { id: "3", label: "Crash on launch", branch: "three" };
interface Search {
query: string;
@@ -40,7 +41,8 @@ function controlledSearch() {
if (!found) throw new Error(`No search for "${query}" yet`);
return found;
};
return { search, forQuery, queries: () => searches.map((candidate) => candidate.query) };
const reject = (index: number, error: Error) => searches[index]?.reject(error);
return { search, forQuery, reject, queries: () => searches.map((candidate) => candidate.query) };
}
const toast = { error: vi.fn() } as unknown as ToastApi;
@@ -124,8 +126,28 @@ describe("plugin Combobox", () => {
await waitFor(() => expect(screen.queryByText("Slow sidebar")).toBeNull());
controlled.forQuery("c").resolve([ONE]);
await new Promise((resolve) => setTimeout(resolve, 50));
expect(screen.getByText("Crash on resume")).toBeTruthy();
// Searches settle in order, so once a later one has landed the stale one has been handled.
fireEvent.change(input, { target: { value: "cra" } });
await waitFor(() => expect(controlled.queries()).toEqual(["", "c", "cr", "cra"]));
controlled.forQuery("cra").resolve([THREE]);
await screen.findByText("Crash on launch");
expect(screen.queryByText("Slow sidebar")).toBeNull();
});
it("shows the error and not the previous results when a reopened list fails to refresh", async () => {
const controlled = controlledSearch();
renderCombobox({ search: controlled.search });
await waitFor(() => expect(controlled.queries()).toEqual([""]));
controlled.forQuery("").resolve([ONE]);
await screen.findByText("Slow sidebar");
fireEvent.keyDown(screen.getByRole("textbox"), { key: "Escape" });
await waitFor(() => expect(screen.queryByRole("textbox")).toBeNull());
fireEvent.click(screen.getByRole("button", { name: "Issue" }));
await waitFor(() => expect(controlled.queries()).toEqual(["", ""]));
controlled.reject(1, new Error("tracker is down"));
expect(await screen.findByText("Search failed: tracker is down")).toBeTruthy();
expect(screen.queryByText("Slow sidebar")).toBeNull();
});
@@ -148,7 +148,8 @@ export function Combobox<Item extends ComboboxItem>({
gcTime: 0,
retry: false,
});
const items: readonly Item[] = results.data ?? NO_RESULTS;
// A failed search shows its error, not the results of an earlier one.
const items: readonly Item[] = results.isError ? NO_RESULTS : (results.data ?? NO_RESULTS);
const item = useMemo<ComposerControlItem>(() => {
const choices = items.map(({ id, label, description }) => ({ value: id, label, description }));
return {
+1
View File
@@ -81,6 +81,7 @@ Use this table as the complete registration checklist.
| `plugin.addClientSide(fn)` in the old root entry | Delete the wrapper and move the body of `fn` into the default client entry function |
| `client.addComposerPill(pill)` inside the old client callback | `client.addComposerPill(pill)` inside `index.client.tsx` or an imported `client/` function |
| New composer item contribution | `client.addComposerItem(item)` in `index.client.tsx`; see [composer items](reference#composer-items) |
| New New workspace item contribution | `client.addNewWorkspaceItem(item)` in `index.client.tsx`; see [New workspace items](reference#new-workspace-items) |
| New header contribution | `client.addHeaderButton({ id, workspaceId, button })` |
| `plugin.addAttachmentSource(source)` in the old root entry | `client.addAttachmentSource(source)` in `index.client.tsx` |
| New settings screen contribution | `client.addSettingsScreen(screen)` in `index.client.tsx`; see [settings screens](reference#settings-screens) |
+1
View File
@@ -74,6 +74,7 @@ my-plugin/
| 旧根入口中的 `plugin.addClientSide(fn)` | 删除这层包装,把 `fn` 的函数体移入默认的客户端入口函数 |
| 旧客户端回调中的 `client.addComposerPill(pill)` | `index.client.tsx` 或被导入的 `client/` 函数中的 `client.addComposerPill(pill)` |
| 新的编辑器项贡献 | `index.client.tsx` 中的 `client.addComposerItem(item)`;见[编辑器项](reference#composer-items) |
| 新的“新建工作区”项贡献 | `index.client.tsx` 中的 `client.addNewWorkspaceItem(item)`;见[新建工作区项](reference#new-workspace-items) |
| 新的标题栏贡献 | `client.addHeaderButton({ id, workspaceId, button })` |
| 旧根入口中的 `plugin.addAttachmentSource(source)` | `index.client.tsx` 中的 `client.addAttachmentSource(source)` |
| 新的设置界面贡献 | `index.client.tsx` 中的 `client.addSettingsScreen(screen)`;见[设置界面](reference#settings-screens) |