mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
## What changed - unify Cmd+K and channel Cmd+F around a removable channel or conversation scope - add conservative fuzzy matching for people and channels while preserving exact-match ordering - make scoped message search complete for one-character queries and expose up to 40 scrollable results - keep the pre-scope channel or DM action in the normal results flow so it scrolls away with the list ## Validation - desktop TypeScript typecheck - desktop text-size and file-size guards - focused fuzzy-search unit tests (24 passed) - focused search Playwright coverage (7 passed), including channel and DM copy, one-character results/no-results, 40-result scrolling, and the non-sticky scope action - desktop E2E build - visual review of channel, scoped, expanded-results, and DM states --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz>
895 lines
30 KiB
TypeScript
895 lines
30 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
import { installMockBridge, openCreateChannelDialog } from "../helpers/bridge";
|
|
|
|
async function getTimelineMetrics(page: import("@playwright/test").Page) {
|
|
return page.getByTestId("message-timeline").evaluate((element) => {
|
|
const timeline = element as HTMLDivElement;
|
|
|
|
return {
|
|
clientHeight: timeline.clientHeight,
|
|
scrollHeight: timeline.scrollHeight,
|
|
scrollTop: timeline.scrollTop,
|
|
distanceFromBottom:
|
|
timeline.scrollHeight - timeline.clientHeight - timeline.scrollTop,
|
|
};
|
|
});
|
|
}
|
|
|
|
async function ensureTimelineScrollable(
|
|
page: import("@playwright/test").Page,
|
|
prefix: string,
|
|
) {
|
|
const input = page.getByTestId("message-input");
|
|
const sendButton = page.getByTestId("send-message");
|
|
|
|
for (let index = 0; index < 24; index += 1) {
|
|
const metrics = await getTimelineMetrics(page);
|
|
if (metrics.scrollHeight > metrics.clientHeight + 160) {
|
|
return;
|
|
}
|
|
|
|
const message = `${prefix} seed ${index}`;
|
|
|
|
await input.fill(message);
|
|
await sendButton.click();
|
|
await expect(page.getByTestId("message-timeline")).toContainText(message);
|
|
}
|
|
|
|
const metrics = await getTimelineMetrics(page);
|
|
expect(metrics.scrollHeight).toBeGreaterThan(metrics.clientHeight + 160);
|
|
}
|
|
|
|
async function focusSidebarSearchWithShortcut(
|
|
page: import("@playwright/test").Page,
|
|
) {
|
|
const openSearchButton = page.getByTestId("open-search");
|
|
|
|
await expect(openSearchButton).toBeVisible();
|
|
await page.evaluate(() => {
|
|
const isMac = /mac|iphone|ipad|ipod/i.test(navigator.platform);
|
|
window.dispatchEvent(
|
|
new KeyboardEvent("keydown", {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
code: "KeyK",
|
|
ctrlKey: !isMac,
|
|
key: "k",
|
|
metaKey: isMac,
|
|
}),
|
|
);
|
|
});
|
|
await expect(page.getByTestId("search-results")).toBeVisible();
|
|
await expect(page.getByTestId("search-dialog-input")).toBeFocused();
|
|
}
|
|
|
|
async function expectHomeView(page: import("@playwright/test").Page) {
|
|
await expect(page.getByTestId("home-inbox-list")).toBeVisible();
|
|
}
|
|
|
|
async function selectHomeInboxFilter(
|
|
page: import("@playwright/test").Page,
|
|
label: "Agents",
|
|
) {
|
|
await page
|
|
.getByTestId("home-inbox")
|
|
.getByRole("button", {
|
|
name: /^Filter inbox:/,
|
|
})
|
|
.click();
|
|
await page.getByRole("menuitemradio", { name: label }).click();
|
|
}
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await installMockBridge(page);
|
|
});
|
|
|
|
test("loads the app shell with mocked channels", async ({ page }) => {
|
|
await page.goto("/");
|
|
|
|
await expect(page.getByTestId("app-sidebar")).toBeVisible();
|
|
await expect(page.getByTestId("stream-list")).toContainText("general");
|
|
await expect(page.getByTestId("forum-list")).toContainText("watercooler");
|
|
await expect(page.getByTestId("dm-list")).toContainText("alice-tyler");
|
|
});
|
|
|
|
async function chooseSharedComputeProvider(
|
|
page: import("@playwright/test").Page,
|
|
) {
|
|
await page.getByRole("tab", { name: "Customize for this agent" }).click();
|
|
const provider = page.locator("#persona-llm-provider");
|
|
await expect(provider).toBeVisible({ timeout: 10_000 });
|
|
await provider.press("Enter");
|
|
await page
|
|
.getByRole("menuitemradio", {
|
|
exact: true,
|
|
name: "Buzz shared compute",
|
|
})
|
|
.click();
|
|
}
|
|
|
|
test("creates a new mocked stream", async ({ page }) => {
|
|
const channelName = `release-notes-${Date.now()}`;
|
|
|
|
await page.goto("/");
|
|
await openCreateChannelDialog(page);
|
|
await page.getByTestId("create-channel-name").fill(channelName);
|
|
await page
|
|
.getByTestId("create-channel-description")
|
|
.fill("Release coordination");
|
|
await page.getByTestId("create-channel-submit").click();
|
|
|
|
await expect(page.getByTestId("stream-list")).toContainText(channelName);
|
|
await expect(page.getByTestId("chat-title")).toContainText(channelName);
|
|
});
|
|
|
|
test("Buzz shared compute explains automatic model selection", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/");
|
|
await page.evaluate(() => {
|
|
(
|
|
window as Window & {
|
|
__BUZZ_E2E_SET_MESH__?: (mesh: {
|
|
models?: Array<{ id: string; name: string | null }>;
|
|
}) => void;
|
|
}
|
|
).__BUZZ_E2E_SET_MESH__?.({ models: [] });
|
|
});
|
|
await page.getByTestId("open-agents-view").click();
|
|
await page.getByTestId("new-agent-card").click();
|
|
await chooseSharedComputeProvider(page);
|
|
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(
|
|
() =>
|
|
(window as Window & { __BUZZ_E2E_COMMANDS__?: string[] })
|
|
.__BUZZ_E2E_COMMANDS__ ?? [],
|
|
),
|
|
)
|
|
.toContain("discover_agent_models");
|
|
await expect(page.locator("#persona-model")).toContainText("Automatic");
|
|
await expect(
|
|
page.getByText(
|
|
"Auto uses Mesh collective intelligence when two or more models stay available, otherwise it chooses one available model.",
|
|
),
|
|
).toBeVisible();
|
|
await expect(page.locator("#persona-custom-model")).toHaveCount(0);
|
|
});
|
|
|
|
test("create agent persists Buzz shared compute with auto model", async ({
|
|
page,
|
|
}) => {
|
|
const agentName = `Shared compute agent ${Date.now()}`;
|
|
|
|
await page.goto("/");
|
|
await page.getByTestId("open-agents-view").click();
|
|
await page.getByTestId("new-agent-card").click();
|
|
await page.locator("#persona-display-name").fill(agentName);
|
|
|
|
await chooseSharedComputeProvider(page);
|
|
|
|
const model = page.locator("#persona-model");
|
|
await expect(model).toContainText("Automatic");
|
|
await page.getByTestId("persona-dialog-submit").click();
|
|
const createdToast = page
|
|
.locator("[data-sonner-toast][data-removed='false']")
|
|
.filter({ hasText: "Agent created" });
|
|
await expect(createdToast).toBeVisible({ timeout: 10_000 });
|
|
await expect(createdToast).toHaveCount(1);
|
|
await expect(page.getByRole("dialog")).toHaveCount(0);
|
|
|
|
const createPayload = await page.evaluate((name) => {
|
|
const log = (
|
|
window as Window & {
|
|
__BUZZ_E2E_COMMAND_LOG__?: Array<{
|
|
command: string;
|
|
payload: unknown;
|
|
}>;
|
|
}
|
|
).__BUZZ_E2E_COMMAND_LOG__;
|
|
return log
|
|
?.filter((entry) => entry.command === "create_managed_agent")
|
|
.map((entry) => entry.payload as { input?: Record<string, unknown> })
|
|
.find((payload) => payload.input?.name === name)?.input;
|
|
}, agentName);
|
|
|
|
expect(createPayload).toMatchObject({
|
|
agentCommand: "buzz-agent",
|
|
model: "auto",
|
|
provider: "relay-mesh",
|
|
spawnAfterCreate: true,
|
|
startOnAppLaunch: true,
|
|
});
|
|
});
|
|
|
|
test("create agent supports parallelism and system prompt overrides", async ({
|
|
page,
|
|
}) => {
|
|
const agentName = `Parallel agent ${Date.now()}`;
|
|
|
|
await page.goto("/");
|
|
await page.getByTestId("open-agents-view").click();
|
|
await page.getByTestId("new-agent-card").click();
|
|
|
|
await page.locator("#persona-display-name").fill(agentName);
|
|
await page
|
|
.locator("#persona-system-prompt")
|
|
.fill("You are concise and parallelize independent work.");
|
|
|
|
// The buzz-agent runtime auto-selects once the ACP runtime catalog loads;
|
|
// Customize reveals the per-agent LLM provider and model fields.
|
|
await page.getByRole("tab", { name: "Customize for this agent" }).click();
|
|
const llmProvider = page.locator("#persona-llm-provider");
|
|
await expect(llmProvider).toBeVisible({ timeout: 10_000 });
|
|
await llmProvider.press("Enter");
|
|
await page
|
|
.getByRole("menuitemradio", { exact: true, name: "Anthropic" })
|
|
.click();
|
|
const model = page.locator("#persona-model");
|
|
await model.click();
|
|
await page
|
|
.getByRole("button", { name: "Custom model...", exact: true })
|
|
.click();
|
|
await page.getByLabel("Custom model ID").fill("claude-opus-4-5");
|
|
await page.getByLabel("Anthropic API Key").fill("sk-test-api-key-for-e2e");
|
|
|
|
const advancedToggle = page.getByRole("button", {
|
|
name: "Advanced",
|
|
exact: true,
|
|
});
|
|
await advancedToggle.click();
|
|
// Parallelism is above the env-vars editor in the Advanced section; filling
|
|
// the required API-key row may have scrolled the dialog past it. Scroll back.
|
|
await page
|
|
.locator("#persona-parallelism")
|
|
.evaluate((el) => el.scrollIntoView({ block: "nearest" }));
|
|
await expect(page.locator("#persona-parallelism")).toBeVisible();
|
|
await page.locator("#persona-parallelism").fill("3");
|
|
|
|
// Submitting mints a running instance whose behavioral quad resolves from
|
|
// the definition (agents always start after creation).
|
|
await page.getByTestId("persona-dialog-submit").click();
|
|
|
|
const createdToast = page
|
|
.locator("[data-sonner-toast][data-removed='false']")
|
|
.filter({ hasText: "Agent created" });
|
|
await expect(createdToast).toBeVisible({ timeout: 10_000 });
|
|
await expect(createdToast).toHaveCount(1);
|
|
await expect(page.getByRole("dialog")).toHaveCount(0);
|
|
|
|
await expect(page.getByTestId("agents-library-personas")).toContainText(
|
|
agentName,
|
|
);
|
|
|
|
// Logs now live in the profile sidebar (PR #1274), not an inline panel.
|
|
// Open the new agent's card to reveal the profile panel, then read the
|
|
// harness log from the diagnostics view.
|
|
await page
|
|
.getByRole("button", { name: `${agentName} agent profile` })
|
|
.click();
|
|
await expect(page.getByTestId("user-profile-panel")).toBeVisible();
|
|
|
|
await page.getByTestId("user-profile-tab-runtime").click();
|
|
await page.getByTestId("user-profile-diagnostics-ingress").click();
|
|
|
|
const log = page.getByTestId("managed-agent-log-content");
|
|
await expect(log).toContainText("parallelism=3");
|
|
await expect(log).toContainText("system prompt override configured");
|
|
});
|
|
|
|
test("opens a mocked channel from the inbox feed", async ({ page }) => {
|
|
const inboxList = page.getByTestId("home-inbox-list");
|
|
|
|
await page.goto("/");
|
|
|
|
await expectHomeView(page);
|
|
await expect(inboxList).toContainText("Please review the release checklist.");
|
|
|
|
const releaseRow = page.getByTestId("home-inbox-item-mock-feed-mention");
|
|
await releaseRow.hover();
|
|
await releaseRow.getByRole("button", { name: "Open in channel" }).click();
|
|
|
|
await expect(page).toHaveURL(
|
|
/#\/channels\/9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50\?messageId=mock-feed-mention$/,
|
|
);
|
|
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
|
});
|
|
|
|
test("Inbox excludes generic channel and unowned agent traffic", async ({
|
|
page,
|
|
}) => {
|
|
const inboxList = page.getByTestId("home-inbox-list");
|
|
|
|
await page.goto("/");
|
|
await expectHomeView(page);
|
|
|
|
await expect(inboxList).not.toContainText(
|
|
"Engineering shipped the desktop build.",
|
|
);
|
|
await expect(inboxList).not.toContainText(
|
|
"Agent progress: channel index complete.",
|
|
);
|
|
|
|
await selectHomeInboxFilter(page, "Agents");
|
|
await expect(inboxList).toContainText("No agent updates found");
|
|
});
|
|
|
|
test("inbox feed renders resolved author labels", async ({ page }) => {
|
|
await page.goto("/");
|
|
|
|
await expect(page.getByTestId("home-inbox-list")).toContainText("alice");
|
|
await expect(page.getByTestId("home-inbox-list")).not.toContainText("You");
|
|
});
|
|
|
|
test("opens sidebar search with the shortcut and loads the exact result", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/");
|
|
|
|
await focusSidebarSearchWithShortcut(page);
|
|
|
|
await page.getByTestId("search-dialog-input").fill("shipped");
|
|
await expect(page.getByTestId("search-results")).toContainText(
|
|
"Engineering shipped the desktop build.",
|
|
);
|
|
|
|
await page.keyboard.press("Enter");
|
|
|
|
await expect(page).toHaveURL(
|
|
/#\/channels\/1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9\?messageId=mock-engineering-shipped$/,
|
|
);
|
|
await expect(page.getByTestId("chat-title")).toHaveText("engineering");
|
|
await expect(page.getByTestId("message-timeline")).toContainText(
|
|
"Engineering shipped the desktop build.",
|
|
);
|
|
});
|
|
|
|
test("opens channel matches from search", async ({ page }) => {
|
|
await page.goto("/");
|
|
|
|
await focusSidebarSearchWithShortcut(page);
|
|
|
|
await page.getByTestId("search-dialog-input").fill("engineering");
|
|
const results = page.getByTestId("search-results");
|
|
|
|
await expect(results).toContainText("engineering");
|
|
await expect(results).toContainText("Engineering discussions");
|
|
await expect(results).toContainText(
|
|
"Design system and UX discussions with engineering partners",
|
|
);
|
|
await expect(
|
|
results.locator('[data-testid^="search-result-channel-"]').first(),
|
|
).toHaveAttribute(
|
|
"data-testid",
|
|
"search-result-channel-1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9",
|
|
);
|
|
|
|
await expect(
|
|
results.getByTestId(
|
|
"search-result-channel-1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9",
|
|
),
|
|
).toHaveAttribute("aria-selected", "true");
|
|
await page.keyboard.press("Enter");
|
|
|
|
await expect(page).toHaveURL(
|
|
/#\/channels\/1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9$/,
|
|
);
|
|
await expect(page.getByTestId("chat-title")).toHaveText("engineering");
|
|
});
|
|
|
|
test("global search offers an optional current-channel scope", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/");
|
|
await page.getByTestId("channel-general").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
|
await expect(page).toHaveURL(
|
|
/#\/channels\/9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50$/,
|
|
);
|
|
|
|
await focusSidebarSearchWithShortcut(page);
|
|
|
|
const scopeControl = page.getByTestId("search-current-channel-control");
|
|
const input = page.getByTestId("search-dialog-input");
|
|
await expect
|
|
.poll(() =>
|
|
scopeControl
|
|
.getByTestId("search-current-scope-label")
|
|
.evaluate((element) => element.textContent),
|
|
)
|
|
.toBe("Search in #general");
|
|
await expect(scopeControl).toContainText("Search in");
|
|
await expect(scopeControl).toContainText("#general");
|
|
await expect(scopeControl).toContainText("Search messages in this channel");
|
|
await expect(page.getByTestId("search-dialog-input-row")).toHaveCSS(
|
|
"border-bottom-width",
|
|
"1px",
|
|
);
|
|
await expect(scopeControl.locator("..")).toHaveCSS(
|
|
"border-bottom-width",
|
|
"0px",
|
|
);
|
|
await expect(scopeControl.locator("..")).toHaveCSS("padding-top", "14px");
|
|
await expect(scopeControl.locator("..")).toHaveCSS("padding-bottom", "14px");
|
|
const [controlBox, dialogBox] = await Promise.all([
|
|
scopeControl.boundingBox(),
|
|
page.getByTestId("search-results").boundingBox(),
|
|
]);
|
|
expect(controlBox).not.toBeNull();
|
|
expect(dialogBox).not.toBeNull();
|
|
expect(controlBox?.width ?? 0).toBeGreaterThan((dialogBox?.width ?? 0) * 0.9);
|
|
expect(controlBox?.width ?? 0).toBeLessThan(dialogBox?.width ?? 0);
|
|
await expect(scopeControl).toHaveAttribute("aria-selected", "true");
|
|
const firstRecentResult = page.locator(".search-result-row").first();
|
|
await input.press("ArrowDown");
|
|
await expect(firstRecentResult).toHaveAttribute("aria-selected", "true");
|
|
await input.press("ArrowUp");
|
|
await expect(scopeControl).toHaveAttribute("aria-selected", "true");
|
|
await input.press("Enter");
|
|
|
|
const scopeChip = page.getByTestId("search-channel-scope-chip");
|
|
await expect(scopeChip).toHaveText(/#general/);
|
|
await expect(input).toBeFocused();
|
|
await input.fill("w");
|
|
const relevantHeader = page.getByText("Most relevant", { exact: true });
|
|
const firstScopedResult = page
|
|
.locator('[data-search-section="messages"] .search-result-row')
|
|
.first();
|
|
await expect(page.getByText("Welcome to #general")).toBeVisible();
|
|
await expect(page.getByText(/Searching messages in/)).toHaveCount(0);
|
|
await expect(relevantHeader).toBeVisible();
|
|
await expect(firstScopedResult).toBeVisible();
|
|
const contentStart = (element: HTMLElement) => {
|
|
const styles = window.getComputedStyle(element);
|
|
return (
|
|
element.getBoundingClientRect().left +
|
|
Number.parseFloat(styles.paddingLeft)
|
|
);
|
|
};
|
|
const [inputStart, headerStart, resultStart] = await Promise.all([
|
|
page.getByTestId("search-dialog-input-row").evaluate(contentStart),
|
|
relevantHeader.evaluate(contentStart),
|
|
firstScopedResult.evaluate(contentStart),
|
|
]);
|
|
expect(Math.abs(inputStart - headerStart)).toBeLessThanOrEqual(1);
|
|
expect(Math.abs(inputStart - resultStart)).toBeLessThanOrEqual(1);
|
|
|
|
await input.fill("x");
|
|
await expect(page.getByTestId("search-results")).toContainText(
|
|
"No messages for x in #general.",
|
|
);
|
|
|
|
await scopeChip.click();
|
|
await expect(scopeChip).toHaveCount(0);
|
|
await expect(input).toBeFocused();
|
|
await input.fill("shipped");
|
|
await expect(page.getByTestId("search-results")).toContainText(
|
|
"Engineering shipped the desktop build.",
|
|
);
|
|
});
|
|
|
|
test("global search offers a conversation-specific scope in direct messages", async ({
|
|
page,
|
|
}) => {
|
|
const directMessageId = "f48efb06-0c93-5025-aac9-2e646bb6bfa8";
|
|
|
|
await page.goto("/");
|
|
await page.getByTestId("channel-alice-tyler").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("alice-tyler");
|
|
await expect(page).toHaveURL(
|
|
new RegExp(`#\\/channels\\/${directMessageId}$`),
|
|
);
|
|
|
|
await focusSidebarSearchWithShortcut(page);
|
|
|
|
const scopeControl = page.getByTestId("search-current-channel-control");
|
|
await expect
|
|
.poll(() =>
|
|
scopeControl
|
|
.getByTestId("search-current-scope-label")
|
|
.evaluate((element) => element.textContent),
|
|
)
|
|
.toBe("Search conversation with alice");
|
|
await expect(scopeControl).toContainText("Search conversation with alice");
|
|
await expect(scopeControl).toContainText(
|
|
"Search messages in this conversation.",
|
|
);
|
|
await expect(scopeControl).not.toContainText("channel");
|
|
await expect(page.getByTestId("search-dialog-input-row")).toHaveCSS(
|
|
"border-bottom-width",
|
|
"1px",
|
|
);
|
|
await expect(scopeControl.locator("..")).toHaveCSS(
|
|
"border-bottom-width",
|
|
"0px",
|
|
);
|
|
await expect(scopeControl.locator("..")).toHaveCSS("padding-top", "14px");
|
|
await expect(scopeControl.locator("..")).toHaveCSS("padding-bottom", "14px");
|
|
|
|
await scopeControl.click();
|
|
|
|
const scopeChip = page.getByTestId("search-channel-scope-chip");
|
|
const input = page.getByTestId("search-dialog-input");
|
|
await expect(scopeChip).toHaveText(/^alice$/);
|
|
await expect(scopeChip).not.toContainText("#");
|
|
await expect(input).toBeFocused();
|
|
await input.fill("a");
|
|
await expect(page.getByTestId("search-results")).toContainText(
|
|
"No messages for a in alice.",
|
|
);
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(() => {
|
|
const calls =
|
|
(
|
|
window as Window & {
|
|
__BUZZ_E2E_COMMAND_LOG__?: Array<{
|
|
command: string;
|
|
payload: unknown;
|
|
}>;
|
|
}
|
|
).__BUZZ_E2E_COMMAND_LOG__ ?? [];
|
|
|
|
return calls.findLast((entry) => entry.command === "search_messages")
|
|
?.payload;
|
|
}),
|
|
)
|
|
.toMatchObject({
|
|
channelId: directMessageId,
|
|
q: "a",
|
|
});
|
|
|
|
await page.keyboard.press("Escape");
|
|
await page.keyboard.press("ControlOrMeta+f");
|
|
await expect(page.getByTestId("search-results")).toBeVisible();
|
|
await expect(page.getByTestId("search-channel-scope-chip")).toHaveText(
|
|
/^alice$/,
|
|
);
|
|
await expect(page.getByTestId("search-current-channel-control")).toHaveCount(
|
|
0,
|
|
);
|
|
});
|
|
|
|
test("channel find shortcut opens unified search with scope selected", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/");
|
|
await page.getByTestId("channel-general").click();
|
|
await expect(page).toHaveURL(
|
|
/#\/channels\/9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50$/,
|
|
);
|
|
|
|
await page.keyboard.press("ControlOrMeta+f");
|
|
|
|
await expect(page.getByTestId("search-results")).toBeVisible();
|
|
await expect(page.getByTestId("search-channel-scope-chip")).toHaveText(
|
|
/#general/,
|
|
);
|
|
await expect(page.getByTestId("search-current-channel-control")).toHaveCount(
|
|
0,
|
|
);
|
|
await expect(page.getByTestId("search-dialog-input")).toBeFocused();
|
|
});
|
|
|
|
test("global search omits channel scoping when no channel is active", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/");
|
|
await expectHomeView(page);
|
|
|
|
await focusSidebarSearchWithShortcut(page);
|
|
|
|
await expect(page.getByTestId("search-current-channel-control")).toHaveCount(
|
|
0,
|
|
);
|
|
await expect(page.getByTestId("search-channel-scope-chip")).toHaveCount(0);
|
|
});
|
|
|
|
test("global one-character search does not query the relay", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/");
|
|
await focusSidebarSearchWithShortcut(page);
|
|
|
|
await page.getByTestId("search-dialog-input").fill("x");
|
|
await page.waitForTimeout(400);
|
|
|
|
const messageSearchCalls = await page.evaluate(() => {
|
|
const calls =
|
|
(
|
|
window as Window & {
|
|
__BUZZ_E2E_COMMAND_LOG__?: Array<{ command: string }>;
|
|
}
|
|
).__BUZZ_E2E_COMMAND_LOG__ ?? [];
|
|
return calls.filter((entry) => entry.command === "search_messages").length;
|
|
});
|
|
expect(messageSearchCalls).toBe(0);
|
|
});
|
|
|
|
test("global search tolerates small channel and people typos", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/");
|
|
await focusSidebarSearchWithShortcut(page);
|
|
|
|
const input = page.getByTestId("search-dialog-input");
|
|
const results = page.getByTestId("search-results");
|
|
await input.fill("engneering");
|
|
await expect(results).toContainText("Engineering discussions");
|
|
|
|
await input.fill("alcie");
|
|
await expect(results).toContainText("alice");
|
|
});
|
|
|
|
test("global search exposes a larger scrollable result window", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/");
|
|
await page.getByTestId("channel-deep-history").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("deep-history");
|
|
await expect(
|
|
page.locator('[data-message-id^="mock-deep-history-"]').first(),
|
|
).toBeVisible();
|
|
|
|
await focusSidebarSearchWithShortcut(page);
|
|
await page.getByTestId("search-dialog-input").fill("deep history message");
|
|
|
|
const resultRows = page.locator(
|
|
'[data-search-section="messages"] .search-result-row',
|
|
);
|
|
await expect(resultRows).toHaveCount(40);
|
|
const resultList = page.getByTestId("search-results-list");
|
|
await expect(resultList).toBeVisible();
|
|
const scopeControl = page.getByTestId("search-current-channel-control");
|
|
await expect(
|
|
resultList.getByTestId("search-current-channel-control"),
|
|
).toBeVisible();
|
|
const dimensions = await resultList.evaluate((element) => ({
|
|
clientHeight: element.clientHeight,
|
|
scrollHeight: element.scrollHeight,
|
|
}));
|
|
expect(dimensions.scrollHeight).toBeGreaterThan(dimensions.clientHeight);
|
|
await resultList.evaluate((element) => {
|
|
element.scrollTop = element.scrollHeight;
|
|
});
|
|
await expect(scopeControl).not.toBeInViewport();
|
|
await resultList.evaluate((element) => {
|
|
element.scrollTop = 0;
|
|
});
|
|
|
|
for (let index = 0; index < 14; index += 1) {
|
|
await page.keyboard.press("ArrowDown");
|
|
}
|
|
await expect(resultRows.nth(13)).toHaveAttribute("aria-selected", "true");
|
|
await expect(resultRows.nth(13)).toBeInViewport();
|
|
});
|
|
|
|
test("closes sidebar search with Escape", async ({ page }) => {
|
|
await page.goto("/");
|
|
|
|
await focusSidebarSearchWithShortcut(page);
|
|
await page.getByTestId("search-dialog-input").fill("shipped");
|
|
|
|
await page.keyboard.press("Escape");
|
|
|
|
await expect(page.getByTestId("search-results")).toHaveCount(0);
|
|
await expect(page.getByTestId("open-search")).toBeFocused();
|
|
});
|
|
|
|
test("search shortcut opens search without disturbing the collapsed sidebar", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/");
|
|
|
|
await expect(page.getByTestId("open-search")).toBeVisible();
|
|
|
|
const sidebarRoot = page.locator('[data-side="left"][data-state]');
|
|
await expect(sidebarRoot).toHaveAttribute("data-state", "expanded");
|
|
|
|
// Collapse the sidebar; its pinned-header search slides off-screen.
|
|
await page
|
|
.getByRole("button", { name: "Toggle Sidebar", exact: true })
|
|
.click();
|
|
await expect(sidebarRoot).toHaveAttribute("data-state", "collapsed");
|
|
|
|
await page.evaluate(() => {
|
|
const isMac = /mac|iphone|ipad|ipod/i.test(navigator.platform);
|
|
window.dispatchEvent(
|
|
new KeyboardEvent("keydown", {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
code: "KeyK",
|
|
ctrlKey: !isMac,
|
|
key: "k",
|
|
metaKey: isMac,
|
|
}),
|
|
);
|
|
});
|
|
|
|
// Search opens in its portal dialog; the sidebar must not react.
|
|
await expect(page.getByTestId("search-dialog-input")).toBeFocused();
|
|
await expect(sidebarRoot).toHaveAttribute("data-state", "collapsed");
|
|
});
|
|
|
|
test("search results use your resolved profile label instead of You", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/");
|
|
|
|
await focusSidebarSearchWithShortcut(page);
|
|
|
|
await page.getByTestId("search-dialog-input").fill("welcome");
|
|
const results = page.getByTestId("search-results");
|
|
|
|
await expect(results).toContainText("Welcome to #general");
|
|
await expect(results).toContainText("npub1mock...");
|
|
await expect(results).not.toContainText("You");
|
|
});
|
|
|
|
test("opens accessible unjoined channels from search in read-only mode", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/");
|
|
|
|
await focusSidebarSearchWithShortcut(page);
|
|
|
|
await page.getByTestId("search-dialog-input").fill("critique");
|
|
const results = page.getByTestId("search-results");
|
|
|
|
await expect(results).toContainText(
|
|
"Design critique notes for the browse flow.",
|
|
);
|
|
await results.getByText("Design critique notes for the browse flow.").click();
|
|
|
|
await expect(page.getByTestId("chat-title")).toHaveText("design");
|
|
await expect(page.getByTestId("message-timeline")).toContainText(
|
|
"Design critique notes for the browse flow.",
|
|
);
|
|
await expect(page.getByTestId("join-banner")).toBeVisible();
|
|
});
|
|
|
|
test("replaces the channel pane when switching channels", async ({ page }) => {
|
|
await page.goto("/");
|
|
|
|
await page.getByTestId("channel-general").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
|
await expect(page.getByTestId("message-timeline")).toContainText(
|
|
"Welcome to #general",
|
|
);
|
|
|
|
await page.getByTestId("channel-random").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("random");
|
|
await expect(page.getByTestId("message-channel-intro")).toBeVisible();
|
|
await expect(page.getByTestId("message-channel-intro")).toContainText(
|
|
"This is the beginning of the regular channel.",
|
|
);
|
|
await expect(page.getByTestId("message-timeline")).not.toContainText(
|
|
"Welcome to #general",
|
|
);
|
|
await expect(page.getByTestId("message-timeline")).toHaveCount(1);
|
|
await expect(page.getByTestId("message-timeline-day-divider")).toHaveCount(0);
|
|
|
|
await page.getByTestId("channel-engineering").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("engineering");
|
|
await expect(page.getByTestId("message-channel-intro")).toBeVisible();
|
|
await expect(page.getByTestId("message-timeline")).toHaveCount(1);
|
|
await expect(page.getByTestId("message-timeline-day-divider")).toHaveCount(0);
|
|
});
|
|
|
|
test("sends a mocked channel message", async ({ page }) => {
|
|
const message = `Smoke message ${Date.now()}`;
|
|
|
|
await page.goto("/");
|
|
await page.getByTestId("channel-general").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
|
await page.getByTestId("message-input").fill(message);
|
|
await page.getByTestId("send-message").click();
|
|
|
|
await expect(page.getByTestId("message-timeline")).toContainText(message);
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(() => {
|
|
const row = Array.from(
|
|
document.querySelectorAll<HTMLElement>("[data-message-id]"),
|
|
).at(-1);
|
|
const composer = document.querySelector<HTMLElement>(
|
|
'[data-testid="message-composer"]',
|
|
);
|
|
if (!row || !composer) return Number.NEGATIVE_INFINITY;
|
|
return (
|
|
composer.getBoundingClientRect().top -
|
|
row.getBoundingClientRect().bottom
|
|
);
|
|
}),
|
|
)
|
|
.toBeGreaterThanOrEqual(0);
|
|
});
|
|
|
|
test("supports multiline drafts with Ctrl+Enter and sends with Enter", async ({
|
|
page,
|
|
}) => {
|
|
const firstLine = `Shortcut smoke line one ${Date.now()}`;
|
|
const restOfLines = [
|
|
"Shortcut smoke line two",
|
|
"Shortcut smoke line three",
|
|
"Shortcut smoke line four",
|
|
"Shortcut smoke line five",
|
|
];
|
|
const input = page.getByTestId("message-input");
|
|
|
|
await page.goto("/");
|
|
await page.getByTestId("channel-general").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
|
await expect(
|
|
page.getByRole("button", { name: "Send message" }),
|
|
).toBeVisible();
|
|
const initialInputHeight = await input.evaluate(
|
|
(element) => (element as HTMLElement).clientHeight,
|
|
);
|
|
expect(initialInputHeight).toBeLessThan(40);
|
|
await input.fill(firstLine);
|
|
for (const line of restOfLines) {
|
|
await input.press("Shift+Enter");
|
|
await input.pressSequentially(line);
|
|
}
|
|
for (const line of [firstLine, ...restOfLines]) {
|
|
await expect(input).toContainText(line);
|
|
}
|
|
const expandedInputHeight = await input.evaluate(
|
|
(element) => (element as HTMLElement).clientHeight,
|
|
);
|
|
expect(expandedInputHeight).toBeLessThanOrEqual(130);
|
|
await expect(page.getByTestId("message-timeline")).not.toContainText(
|
|
firstLine,
|
|
);
|
|
await input.press("Enter");
|
|
|
|
await expect(page.getByTestId("message-timeline")).toContainText(firstLine);
|
|
await expect(page.getByTestId("message-timeline")).toContainText(
|
|
restOfLines[restOfLines.length - 1],
|
|
);
|
|
});
|
|
|
|
test("does not shift the timeline when the composer grows", async ({
|
|
page,
|
|
}) => {
|
|
const input = page.getByTestId("message-input");
|
|
const prefix = `Composer growth ${Date.now()}`;
|
|
|
|
await page.goto("/");
|
|
await page.getByTestId("channel-general").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
|
|
|
await ensureTimelineScrollable(page, prefix);
|
|
await page.waitForTimeout(400);
|
|
await page.getByTestId("message-timeline").evaluate((element) => {
|
|
const timeline = element as HTMLDivElement;
|
|
// The raw position assignment sets up detached history, while wheel is the
|
|
// same ownership signal a real reader produces before composer reflow.
|
|
timeline.dispatchEvent(new WheelEvent("wheel", { deltaY: -100 }));
|
|
timeline.scrollTop = 0;
|
|
timeline.dispatchEvent(new Event("scroll"));
|
|
});
|
|
await expect
|
|
.poll(async () => (await getTimelineMetrics(page)).distanceFromBottom)
|
|
.toBeGreaterThan(160);
|
|
const before = await getTimelineMetrics(page);
|
|
|
|
await input.fill("Composer growth line one");
|
|
await input.press("Shift+Enter");
|
|
await input.pressSequentially("Composer growth line two");
|
|
await input.press("Shift+Enter");
|
|
await input.pressSequentially("Composer growth line three");
|
|
await input.press("Shift+Enter");
|
|
await input.pressSequentially("Composer growth line four");
|
|
|
|
await page.waitForTimeout(1200);
|
|
|
|
const after = await getTimelineMetrics(page);
|
|
expect(after.clientHeight).toBeLessThanOrEqual(before.clientHeight);
|
|
expect(Math.abs(after.scrollTop - before.scrollTop)).toBeLessThanOrEqual(2);
|
|
expect(after.distanceFromBottom).toBeGreaterThan(160);
|
|
});
|