* feat(app): show host in command center and multi-select host filter Command-center agent results now show which host the workspace lives on, shown only when more than one host is connected (matching the sidebar). The sidebar display-preferences filter becomes multi-select — pick any number of hosts instead of one-or-all — and its host rows now carry a status dot on the left like the other host pickers. Persisted filter state migrates from the old single host to a host list. * test(app): extract host-filter and command-center e2e DSL helpers Move the mechanical menu/search interactions out of the new spec bodies into focused helpers so the tests read as user intent. * fix(app): drop the icon from the host filter "All hosts" item * fix(app): make multi-host e2e seeding order-independent The offline-host seed relied on Playwright running the test's init script after the fixture's registry reset — an ordering Playwright does not guarantee. Write the full registry and set the fixture's disable-once flag, then reload, so the multi-host seed survives regardless of script order. Also tag the pre-v2 hostFilter migration reader with COMPAT so the cleanup sweep can find it.
163 lines
4.4 KiB
TypeScript
163 lines
4.4 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { StateStorage } from "zustand/middleware";
|
|
import {
|
|
createSidebarViewStorage,
|
|
migrateSidebarViewState,
|
|
useSidebarViewStore,
|
|
} from "./sidebar-view-store";
|
|
|
|
vi.mock("@react-native-async-storage/async-storage", () => ({
|
|
default: {
|
|
getItem: vi.fn().mockResolvedValue(null),
|
|
setItem: vi.fn().mockResolvedValue(undefined),
|
|
removeItem: vi.fn().mockResolvedValue(undefined),
|
|
},
|
|
}));
|
|
|
|
interface MemoryStorage extends StateStorage<Promise<void>> {
|
|
reads: string[];
|
|
}
|
|
|
|
function createMemoryStorage(entries: Record<string, string | null>): MemoryStorage {
|
|
const reads: string[] = [];
|
|
return {
|
|
reads,
|
|
getItem: async (name) => {
|
|
reads.push(name);
|
|
return entries[name] ?? null;
|
|
},
|
|
setItem: async (name, value) => {
|
|
entries[name] = value;
|
|
},
|
|
removeItem: async (name) => {
|
|
entries[name] = null;
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("sidebar view store", () => {
|
|
beforeEach(() => {
|
|
useSidebarViewStore.setState({
|
|
groupMode: "project",
|
|
hostFilters: [],
|
|
});
|
|
});
|
|
|
|
it("toggles multiple hosts into and out of the filter", () => {
|
|
const store = useSidebarViewStore.getState();
|
|
store.toggleHostFilter("host-a");
|
|
store.toggleHostFilter("host-b");
|
|
|
|
expect(useSidebarViewStore.getState().hostFilters).toEqual(["host-a", "host-b"]);
|
|
|
|
store.toggleHostFilter("host-a");
|
|
|
|
expect(useSidebarViewStore.getState().hostFilters).toEqual(["host-b"]);
|
|
|
|
store.clearHostFilters();
|
|
|
|
expect(useSidebarViewStore.getState().hostFilters).toEqual([]);
|
|
});
|
|
|
|
it("keeps host filters that still point at available hosts", () => {
|
|
const store = useSidebarViewStore.getState();
|
|
store.toggleHostFilter("host-a");
|
|
store.toggleHostFilter("host-b");
|
|
|
|
store.reconcileHostFilters(["host-a", "host-b", "host-c"]);
|
|
|
|
expect(useSidebarViewStore.getState().hostFilters).toEqual(["host-a", "host-b"]);
|
|
});
|
|
|
|
it("drops a host filter after that host is removed", () => {
|
|
const store = useSidebarViewStore.getState();
|
|
store.toggleHostFilter("host-a");
|
|
store.toggleHostFilter("removed-host");
|
|
|
|
store.reconcileHostFilters(["host-a"]);
|
|
|
|
expect(useSidebarViewStore.getState().hostFilters).toEqual(["host-a"]);
|
|
});
|
|
|
|
it("migrates legacy per-host group modes to the new global mode", () => {
|
|
expect(
|
|
migrateSidebarViewState({
|
|
groupModeByServerId: {
|
|
"host-a": "project",
|
|
"host-b": "status",
|
|
},
|
|
}),
|
|
).toEqual({
|
|
groupMode: "status",
|
|
hostFilters: [],
|
|
});
|
|
});
|
|
|
|
it("migrates a pre-v2 single host filter to the multi-host list", () => {
|
|
expect(
|
|
migrateSidebarViewState({
|
|
groupMode: "status",
|
|
hostFilter: "host-a",
|
|
}),
|
|
).toEqual({
|
|
groupMode: "status",
|
|
hostFilters: ["host-a"],
|
|
});
|
|
});
|
|
|
|
it("keeps current persisted sidebar view state during version migration", () => {
|
|
expect(
|
|
migrateSidebarViewState({
|
|
groupMode: "status",
|
|
hostFilters: ["host-a", "host-b"],
|
|
}),
|
|
).toEqual({
|
|
groupMode: "status",
|
|
hostFilters: ["host-a", "host-b"],
|
|
});
|
|
});
|
|
|
|
it("falls back to the legacy storage key when the new key is empty", async () => {
|
|
const storage = createMemoryStorage({
|
|
"sidebar-view": null,
|
|
"sidebar-group-mode": JSON.stringify({
|
|
state: { groupModeByServerId: { "host-a": "status" } },
|
|
version: 0,
|
|
}),
|
|
});
|
|
|
|
const value = await createSidebarViewStorage(storage).getItem("sidebar-view");
|
|
|
|
expect(value).toBe(
|
|
JSON.stringify({
|
|
state: { groupModeByServerId: { "host-a": "status" } },
|
|
version: 0,
|
|
}),
|
|
);
|
|
expect(storage.reads).toEqual(["sidebar-view", "sidebar-group-mode"]);
|
|
});
|
|
|
|
it("uses the new storage key without reading the legacy key when current state exists", async () => {
|
|
const storage = createMemoryStorage({
|
|
"sidebar-view": JSON.stringify({
|
|
state: { groupMode: "project", hostFilters: ["host-a"] },
|
|
version: 2,
|
|
}),
|
|
"sidebar-group-mode": JSON.stringify({
|
|
state: { groupModeByServerId: { "host-b": "status" } },
|
|
version: 0,
|
|
}),
|
|
});
|
|
|
|
const value = await createSidebarViewStorage(storage).getItem("sidebar-view");
|
|
|
|
expect(value).toBe(
|
|
JSON.stringify({
|
|
state: { groupMode: "project", hostFilters: ["host-a"] },
|
|
version: 2,
|
|
}),
|
|
);
|
|
expect(storage.reads).toEqual(["sidebar-view"]);
|
|
});
|
|
});
|