This repository has been archived on 2026-10-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
maestro/web/src/sidebarSearch.test.ts
T
Miguel PalhasandClaude Sonnet 5 c9562ab5eb Closes #252: port vanilla-DOM tests to React Testing Library
Rewrites the 27 DOM-behavior test files (dialogs, sidebar, spawn form,
command bar, structured pane, catalog, multiview, theme, etc.) to use
screen/within/fireEvent/waitFor instead of manual querySelector and
dispatchEvent. Pure-logic test files (matcher, tree, activity, paths,
reconnect, keys, webgl, paneTitle, commands, daemonLink, events, layout,
persistence, previewSchedule, projectList, spawnSimilar, structuredDiff,
toolIcon, fastForward, spawnForm) are untouched, and preview.test.ts
needed no changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 02:06:31 +01:00

141 lines
4.8 KiB
TypeScript

// @vitest-environment happy-dom
//
// The sidebar's search field is the touch entry point to the command bar
// (#157, docs/COMMAND-BAR.md "Touch entry"): tapping or focusing it asks the
// shell to open the bar with an empty query, instead of filtering the tree.
import "@testing-library/jest-dom/vitest";
import { fireEvent, within } from "@testing-library/react";
import { beforeAll, describe, expect, it } from "vitest";
import { OPEN_COMMAND_BAR_EVENT, renderSidebar } from "./catalog.js";
import { sessionBarCandidates } from "./commandBar.js";
import { matchAndRank } from "./utils/matcher.js";
import type { CatalogListener, CatalogStore } from "./events.js";
import type { Session } from "./types.js";
beforeAll(() => {
Object.defineProperty(window, "localStorage", {
value: { getItem: () => null, setItem: () => {} },
});
window.fetch = () => Promise.reject(new Error("offline"));
});
function session(id: string, cwd: string): Session {
return {
id,
name: id,
cwd,
command: "claude",
env: {},
metadata: {},
status: "Running",
created_at: "2026-08-24T10:00:00Z",
} as Session;
}
function storeWith(sessions: Session[]): CatalogStore {
const state = { sessions: new Map(sessions.map((s) => [s.id, s])) };
return {
snapshot: () => state,
subscribe: (fn: CatalogListener) => {
fn(state);
return () => {};
},
} as unknown as CatalogStore;
}
function mount(sessions: Session[] = [session("alpha", "/home/n/alpha")]) {
const handle = renderSidebar(storeWith(sessions));
document.body.appendChild(handle.el);
const search = within(handle.el).getByRole("searchbox", {
name: "Search sessions",
}) as HTMLInputElement;
const opens: number[] = [];
const listener = () => opens.push(1);
document.addEventListener(OPEN_COMMAND_BAR_EVENT, listener);
return {
handle,
search,
opens,
dispose: () => {
document.removeEventListener(OPEN_COMMAND_BAR_EVENT, listener);
handle.dispose();
handle.el.remove();
},
};
}
describe("sidebar search field opens the command bar (#157)", () => {
it("asks the shell to open the bar when the field is focused", () => {
const t = mount();
fireEvent.focus(t.search);
expect(t.opens).toHaveLength(1);
t.dispose();
});
it("opens on a tap, where a focus event may never arrive", () => {
const t = mount();
fireEvent.pointerDown(t.search);
expect(t.opens).toHaveLength(1);
t.dispose();
});
it("does not trap the keyboard in the field: it gives focus up on open", () => {
const t = mount();
t.search.focus();
expect(t.search).not.toHaveFocus();
t.dispose();
});
it("opens the bar with an empty query — the field never holds one", () => {
const t = mount();
t.search.focus();
expect(t.search.value).toBe("");
t.dispose();
});
it("is readonly: no on-screen keyboard over a field nothing is typed into", () => {
const t = mount();
expect(t.search.readOnly).toBe(true);
t.dispose();
});
it("no longer filters the tree in place", () => {
const t = mount([session("alpha", "/home/n/alpha"), session("beta", "/home/n/beta")]);
fireEvent.input(t.search, { target: { value: "alpha" } });
// Both cards share one command ("claude") and so one accessible name; the
// DEV-only "kit" link would also match a bare role query for "link".
expect(within(t.handle.el).getAllByRole("link", { name: "claude, running, Idle" })).toHaveLength(2);
t.dispose();
});
it("has no 'No matches' state left: an empty catalog is empty, not filtered", () => {
const t = mount([]);
expect(within(t.handle.el).getByText("No sessions yet")).toBeTruthy();
t.dispose();
});
});
// The inline filter matched a session on its name and its directory, case- and
// whitespace-insensitively. That behaviour did not go away with the field — it
// moved into the bar, which ranks the same two fields (docs/COMMAND-BAR.md).
describe("what the retired inline filter did, the bar now does", () => {
it("matches a session on both its name and its directory", () => {
const sessions = [session("alpha", "/home/n/alpha"), session("beta", "/home/n/beta")];
const candidates = sessionBarCandidates(sessions, "/home/n");
const byName = matchAndRank("alpha", candidates).map((r) => r.candidate.session.id);
expect(byName).toEqual(["alpha"]);
const byDir = matchAndRank("beta", candidates).map((r) => r.candidate.session.id);
expect(byDir).toEqual(["beta"]);
});
it("ignores case and surrounding whitespace, and shows nothing on no match", () => {
const candidates = sessionBarCandidates([session("alpha", "/home/n/Alpha")], "/home/n");
expect(matchAndRank(" ALPHA ", candidates)).toHaveLength(1);
expect(matchAndRank("zzz", candidates)).toHaveLength(0);
});
});