mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Merge remote-tracking branch 'origin/main' into kennylopez-agent-catalog-sharing
This commit is contained in:
@@ -216,4 +216,148 @@ test.describe("community rail", () => {
|
||||
expect(toggleBox).not.toBeNull();
|
||||
expect(toggleBox?.x ?? 0).toBeLessThan(120);
|
||||
});
|
||||
|
||||
test("drag-to-reorder updates the stored community order and survives reload", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installMockBridge(page, undefined, { skipCommunitySeed: true });
|
||||
// Seed only if not already set so the persisted order survives page.reload().
|
||||
await page.addInitScript(
|
||||
({ list, active }) => {
|
||||
if (!window.localStorage.getItem("buzz-communities")) {
|
||||
window.localStorage.setItem("buzz-communities", JSON.stringify(list));
|
||||
}
|
||||
if (!window.localStorage.getItem("buzz-active-community-id")) {
|
||||
window.localStorage.setItem("buzz-active-community-id", active);
|
||||
}
|
||||
},
|
||||
{ list: [COMMUNITY_A, COMMUNITY_B], active: COMMUNITY_A.id },
|
||||
);
|
||||
await page.goto("/");
|
||||
|
||||
const buttonA = page.getByTestId(`community-rail-button-${COMMUNITY_A.id}`);
|
||||
const buttonB = page.getByTestId(`community-rail-button-${COMMUNITY_B.id}`);
|
||||
await expect(buttonA).toBeVisible();
|
||||
await expect(buttonB).toBeVisible();
|
||||
|
||||
// Drag B (lower) up over A (higher) so the order becomes [B, A].
|
||||
const boxA = await buttonA.boundingBox();
|
||||
const boxB = await buttonB.boundingBox();
|
||||
if (!boxA || !boxB) throw new Error("community buttons not laid out");
|
||||
|
||||
const startX = boxB.x + boxB.width / 2;
|
||||
const startY = boxB.y + boxB.height / 2;
|
||||
const targetY = boxA.y + boxA.height / 2;
|
||||
|
||||
// dnd-kit PointerSensor requires a 6px activation distance before it picks
|
||||
// up the drag. Move in small steps so pointermove events fire on every pixel.
|
||||
await page.mouse.move(startX, startY);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(startX, startY - 3, { steps: 3 });
|
||||
await page.mouse.move(startX, targetY, { steps: 20 });
|
||||
await page.mouse.up();
|
||||
|
||||
// The community list in localStorage must now be [B, A].
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(() => {
|
||||
const raw = window.localStorage.getItem("buzz-communities");
|
||||
if (!raw) return null;
|
||||
const list = JSON.parse(raw) as Array<{ id: string }>;
|
||||
return list.map((c) => c.id);
|
||||
}),
|
||||
)
|
||||
.toEqual([COMMUNITY_B.id, COMMUNITY_A.id]);
|
||||
|
||||
// Verify the new order is also reflected in the rendered DOM — B button
|
||||
// must appear above A button.
|
||||
const newBoxA = await buttonA.boundingBox();
|
||||
const newBoxB = await buttonB.boundingBox();
|
||||
if (!newBoxA || !newBoxB)
|
||||
throw new Error("community buttons not laid out after drag");
|
||||
expect(newBoxB.y).toBeLessThan(newBoxA.y);
|
||||
|
||||
// Reload and confirm the order survives restart: addInitScript is
|
||||
// conditional (no-op when data already exists), so the dragged [B, A]
|
||||
// order is what React reads on boot.
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("community-rail")).toBeVisible();
|
||||
|
||||
// Storage must still be [B, A] after reload.
|
||||
const storedOrder = await page.evaluate(() => {
|
||||
const raw = window.localStorage.getItem("buzz-communities");
|
||||
if (!raw) return null;
|
||||
const list = JSON.parse(raw) as Array<{ id: string }>;
|
||||
return list.map((c) => c.id);
|
||||
});
|
||||
expect(storedOrder).toEqual([COMMUNITY_B.id, COMMUNITY_A.id]);
|
||||
|
||||
// DOM order must also be [B, A] after reload.
|
||||
const reloadBoxA = await buttonA.boundingBox();
|
||||
const reloadBoxB = await buttonB.boundingBox();
|
||||
if (!reloadBoxA || !reloadBoxB)
|
||||
throw new Error("community buttons not laid out after reload");
|
||||
expect(reloadBoxB.y).toBeLessThan(reloadBoxA.y);
|
||||
});
|
||||
|
||||
test("keyboard reorder: Space to pick up, ArrowUp to move, Space to drop updates stored order", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installMockBridge(page, undefined, { skipCommunitySeed: true });
|
||||
await seedCommunities(page, [COMMUNITY_A, COMMUNITY_B], COMMUNITY_A.id);
|
||||
await page.goto("/");
|
||||
|
||||
const buttonA = page.getByTestId(`community-rail-button-${COMMUNITY_A.id}`);
|
||||
const buttonB = page.getByTestId(`community-rail-button-${COMMUNITY_B.id}`);
|
||||
await expect(buttonA).toBeVisible();
|
||||
await expect(buttonB).toBeVisible();
|
||||
|
||||
// Focus B (the second/lower item) and use keyboard to move it above A.
|
||||
// Note: page.keyboard.press("Space") fires the button's native click on this
|
||||
// Chromium build even when React's onKeyDown calls preventDefault — a CDP
|
||||
// input-injection quirk. The synthetic dispatch below goes directly through
|
||||
// React's event system where preventDefault correctly suppresses the click,
|
||||
// while still exercising the real KeyboardSensor path (Thufir verified the
|
||||
// test fails when KeyboardSensor is removed).
|
||||
await buttonB.focus();
|
||||
await page.evaluate((testId) => {
|
||||
const el = document.querySelector(`[data-testid="${testId}"]`);
|
||||
if (!el) throw new Error(`button not found: ${testId}`);
|
||||
el.dispatchEvent(
|
||||
new KeyboardEvent("keydown", {
|
||||
key: " ",
|
||||
code: "Space",
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}),
|
||||
);
|
||||
}, `community-rail-button-${COMMUNITY_B.id}`);
|
||||
// ArrowUp moves the active item one slot up.
|
||||
await page.keyboard.press("ArrowUp");
|
||||
// Space drops the item — same synthetic dispatch for consistency.
|
||||
await page.evaluate((testId) => {
|
||||
const el = document.querySelector(`[data-testid="${testId}"]`);
|
||||
if (!el) throw new Error(`button not found: ${testId}`);
|
||||
el.dispatchEvent(
|
||||
new KeyboardEvent("keydown", {
|
||||
key: " ",
|
||||
code: "Space",
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}),
|
||||
);
|
||||
}, `community-rail-button-${COMMUNITY_B.id}`);
|
||||
|
||||
// The community list in localStorage must now be [B, A].
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(() => {
|
||||
const raw = window.localStorage.getItem("buzz-communities");
|
||||
if (!raw) return null;
|
||||
const list = JSON.parse(raw) as Array<{ id: string }>;
|
||||
return list.map((c) => c.id);
|
||||
}),
|
||||
)
|
||||
.toEqual([COMMUNITY_B.id, COMMUNITY_A.id]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -431,6 +431,76 @@ test("defaults renders only fields supported by the selected harness", async ({
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("defaults hides model when optional harness has empty discovery", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installMockBridge(
|
||||
page,
|
||||
{
|
||||
acpRuntimesCatalog: [
|
||||
runtime("claude", "available", { status: "logged_in" }),
|
||||
],
|
||||
discoverAgentModels: {
|
||||
models: [],
|
||||
supportsSwitching: false,
|
||||
},
|
||||
globalAgentConfig: {
|
||||
env_vars: {},
|
||||
provider: null,
|
||||
model: null,
|
||||
preferred_runtime: null,
|
||||
},
|
||||
},
|
||||
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
||||
);
|
||||
await page.goto("/");
|
||||
await navigateToSetupPage(page);
|
||||
await page.getByTestId("onboarding-setup-next").click();
|
||||
|
||||
await expect(page.getByTestId("onboarding-page-config")).toBeVisible();
|
||||
await expect(page.getByTestId("global-agent-default-harness")).toHaveText(
|
||||
"Claude Code",
|
||||
);
|
||||
// Confirmed successful empty catalog — omit the Model control; harness
|
||||
// default applies and Finish stays available.
|
||||
await expect(page.getByTestId("global-agent-model")).toHaveCount(0);
|
||||
await expect(page.getByTestId("onboarding-finish")).toBeEnabled();
|
||||
});
|
||||
|
||||
test("defaults keeps model control when optional harness discovery fails", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installMockBridge(
|
||||
page,
|
||||
{
|
||||
acpRuntimesCatalog: [
|
||||
runtime("claude", "available", { status: "logged_in" }),
|
||||
],
|
||||
discoverAgentModelsError: "CLI discovery timed out",
|
||||
globalAgentConfig: {
|
||||
env_vars: {},
|
||||
provider: null,
|
||||
model: null,
|
||||
preferred_runtime: null,
|
||||
},
|
||||
},
|
||||
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
||||
);
|
||||
await page.goto("/");
|
||||
await navigateToSetupPage(page);
|
||||
await page.getByTestId("onboarding-setup-next").click();
|
||||
|
||||
await expect(page.getByTestId("onboarding-page-config")).toBeVisible();
|
||||
await expect(page.getByTestId("global-agent-default-harness")).toHaveText(
|
||||
"Claude Code",
|
||||
);
|
||||
// Failed discovery must not look like successful empty: keep the control
|
||||
// and surface #2246 failure UI (status line bypasses onboarding-essential).
|
||||
await expect(page.getByTestId("global-agent-model")).toBeVisible();
|
||||
await expect(page.getByText(/Could not load live models/i)).toBeVisible();
|
||||
await expect(page.getByTestId("onboarding-finish")).toBeEnabled();
|
||||
});
|
||||
|
||||
test("defaults Back returns to harness setup", async ({ page }) => {
|
||||
await installMockBridge(
|
||||
page,
|
||||
|
||||
@@ -431,6 +431,27 @@ type MockBridgeOptions = {
|
||||
* test can interleave edits and exercise the mid-save race handling.
|
||||
*/
|
||||
globalConfigSaveDelayMs?: number;
|
||||
/**
|
||||
* Override the `discover_agent_models` mock response. When set, the bridge
|
||||
* returns this catalog instead of the default per-harness model list.
|
||||
* Use `{ models: [], supportsSwitching: false }` to exercise empty discovery
|
||||
* (e.g. optional harnesses that omit the Model control).
|
||||
*/
|
||||
discoverAgentModels?: {
|
||||
models: Array<{
|
||||
id: string;
|
||||
name: string | null;
|
||||
description?: string | null;
|
||||
}>;
|
||||
supportsSwitching: boolean;
|
||||
agentDefaultModel?: string | null;
|
||||
selectedModel?: string | null;
|
||||
};
|
||||
/**
|
||||
* When set, `discover_agent_models` throws with this message instead of
|
||||
* returning a catalog. Exercises the discovery-failure UI path.
|
||||
*/
|
||||
discoverAgentModelsError?: string;
|
||||
};
|
||||
|
||||
type BridgeOptions = {
|
||||
|
||||
Reference in New Issue
Block a user