mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
1765 lines
60 KiB
TypeScript
1765 lines
60 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
import { KIND_TYPING_INDICATOR } from "../../src/shared/constants/kinds";
|
|
import {
|
|
TEST_IDENTITIES,
|
|
installMockBridge,
|
|
openChannelBrowser,
|
|
} from "../helpers/bridge";
|
|
|
|
const MOCK_IDENTITY_PUBKEY = "deadbeef".repeat(8);
|
|
|
|
async function openChannelManagement(
|
|
page: import("@playwright/test").Page,
|
|
channelName: string,
|
|
) {
|
|
await page.getByTestId(`channel-${channelName}`).click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText(channelName);
|
|
await page.getByTestId("channel-management-trigger").click();
|
|
await expect(page.getByTestId("channel-management-sheet")).toBeVisible();
|
|
}
|
|
|
|
async function closeChannelManagement(page: import("@playwright/test").Page) {
|
|
await page.keyboard.press("Escape");
|
|
await expect(page.getByTestId("channel-management-sheet")).not.toBeVisible();
|
|
}
|
|
|
|
async function openMembersSidebar(
|
|
page: import("@playwright/test").Page,
|
|
channelName: string,
|
|
) {
|
|
await page.getByTestId(`channel-${channelName}`).click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText(channelName);
|
|
await page.getByTestId("channel-members-trigger").click();
|
|
await expect(page.getByTestId("members-sidebar")).toBeVisible();
|
|
}
|
|
|
|
async function readMembersTriggerCount(page: import("@playwright/test").Page) {
|
|
const label =
|
|
(await page
|
|
.getByTestId("channel-members-trigger")
|
|
.getAttribute("aria-label")) ?? "";
|
|
const match = label.match(/\((\d+)\)$/);
|
|
if (!match) {
|
|
throw new Error(`Could not read member count from label: ${label}`);
|
|
}
|
|
return Number(match[1]);
|
|
}
|
|
|
|
async function expectMembersTriggerCount(
|
|
page: import("@playwright/test").Page,
|
|
count: number,
|
|
) {
|
|
await expect(page.getByTestId("channel-members-trigger")).toHaveAttribute(
|
|
"aria-label",
|
|
`View channel members (${count})`,
|
|
);
|
|
}
|
|
|
|
async function waitForMockLiveSubscription(
|
|
page: import("@playwright/test").Page,
|
|
channelName: string,
|
|
kind?: number,
|
|
) {
|
|
await expect
|
|
.poll(async () => {
|
|
return page.evaluate(
|
|
({ currentChannelName, kind }) => {
|
|
return (
|
|
(
|
|
window as Window & {
|
|
__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?: (input: {
|
|
channelName: string;
|
|
kind?: number;
|
|
}) => boolean;
|
|
}
|
|
).__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?.({
|
|
channelName: currentChannelName,
|
|
kind,
|
|
}) ?? false
|
|
);
|
|
},
|
|
{ currentChannelName: channelName, kind },
|
|
);
|
|
})
|
|
.toBe(true);
|
|
}
|
|
|
|
async function openMemberMenu(
|
|
page: import("@playwright/test").Page,
|
|
pubkey: string,
|
|
) {
|
|
const row = page.getByTestId(`sidebar-member-${pubkey}`);
|
|
const trigger = page.getByTestId(`sidebar-member-menu-${pubkey}`);
|
|
await row.scrollIntoViewIfNeeded();
|
|
await row.hover();
|
|
// Workaround: @radix-ui/react-dropdown-menu@2.1.16 ignores pointer-based
|
|
// re-opens after a menu item click (onCloseAutoFocus race). Opening via
|
|
// keyboard (focus + Enter) is reliable. Revisit if Radix fixes this.
|
|
await trigger.focus();
|
|
await trigger.press("Enter");
|
|
await expect(trigger).toHaveAttribute("data-state", "open");
|
|
}
|
|
|
|
async function addGenericAgent(
|
|
page: import("@playwright/test").Page,
|
|
channelName: string,
|
|
agentName: string,
|
|
): Promise<string> {
|
|
await page.getByTestId(`channel-${channelName}`).click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText(channelName);
|
|
const channelId = await page
|
|
.getByTestId(`channel-${channelName}`)
|
|
.getAttribute("data-channel-id");
|
|
if (!channelId) {
|
|
throw new Error(`Channel ${channelName} is missing a data-channel-id.`);
|
|
}
|
|
|
|
await page.waitForFunction(() => {
|
|
return Boolean(
|
|
(
|
|
window as Window & {
|
|
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: unknown;
|
|
}
|
|
).__BUZZ_E2E_INVOKE_MOCK_COMMAND__,
|
|
);
|
|
});
|
|
return page.evaluate(
|
|
async ({ agentName, channelId }) => {
|
|
const invoke = (
|
|
window as Window & {
|
|
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
|
|
command: string,
|
|
payload?: Record<string, unknown>,
|
|
) => Promise<{ agent?: { pubkey: string } }>;
|
|
}
|
|
).__BUZZ_E2E_INVOKE_MOCK_COMMAND__;
|
|
if (!invoke) {
|
|
throw new Error("Mock bridge is not installed.");
|
|
}
|
|
|
|
const created = await invoke("create_managed_agent", {
|
|
input: {
|
|
name: agentName,
|
|
spawnAfterCreate: true,
|
|
systemPrompt: "Watch the channel and help when asked.",
|
|
},
|
|
});
|
|
const pubkey = created.agent?.pubkey;
|
|
if (!pubkey) {
|
|
throw new Error("Mock managed agent creation did not return a pubkey.");
|
|
}
|
|
|
|
await invoke("add_channel_members", {
|
|
channelId,
|
|
pubkeys: [pubkey],
|
|
role: "bot",
|
|
});
|
|
|
|
await (
|
|
window as Window & {
|
|
__BUZZ_E2E_QUERY_CLIENT__?: {
|
|
invalidateQueries: () => Promise<void>;
|
|
};
|
|
}
|
|
).__BUZZ_E2E_QUERY_CLIENT__?.invalidateQueries();
|
|
|
|
return pubkey;
|
|
},
|
|
{ agentName, channelId },
|
|
);
|
|
}
|
|
|
|
async function readCommandLog(page: import("@playwright/test").Page) {
|
|
return page.evaluate(() => {
|
|
return (
|
|
(window as Window & { __BUZZ_E2E_COMMANDS__?: string[] })
|
|
.__BUZZ_E2E_COMMANDS__ ?? []
|
|
);
|
|
});
|
|
}
|
|
|
|
async function readCommandPayloadLog(page: import("@playwright/test").Page) {
|
|
return page.evaluate(() => {
|
|
return (
|
|
(
|
|
window as Window & {
|
|
__BUZZ_E2E_COMMAND_LOG__?: Array<{
|
|
command: string;
|
|
payload: unknown;
|
|
}>;
|
|
}
|
|
).__BUZZ_E2E_COMMAND_LOG__ ?? []
|
|
);
|
|
});
|
|
}
|
|
|
|
async function invokeMockCommand<T>(
|
|
page: import("@playwright/test").Page,
|
|
command: string,
|
|
payload?: Record<string, unknown>,
|
|
) {
|
|
await page.waitForFunction(() => {
|
|
return Boolean(
|
|
(
|
|
window as Window & {
|
|
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: unknown;
|
|
}
|
|
).__BUZZ_E2E_INVOKE_MOCK_COMMAND__,
|
|
);
|
|
});
|
|
|
|
return page.evaluate(
|
|
async ({
|
|
command,
|
|
payload,
|
|
}: {
|
|
command: string;
|
|
payload?: Record<string, unknown>;
|
|
}) => {
|
|
const invoke = (
|
|
window as Window & {
|
|
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
|
|
command: string,
|
|
payload?: Record<string, unknown>,
|
|
) => Promise<unknown>;
|
|
}
|
|
).__BUZZ_E2E_INVOKE_MOCK_COMMAND__;
|
|
|
|
if (!invoke) {
|
|
throw new Error("Mock bridge is not installed.");
|
|
}
|
|
|
|
return invoke(command, payload);
|
|
},
|
|
{ command, payload },
|
|
) as Promise<T>;
|
|
}
|
|
|
|
async function expectSameLeftInset(
|
|
page: import("@playwright/test").Page,
|
|
firstTestId: string,
|
|
secondTestId: string,
|
|
) {
|
|
const firstBox = await page.getByTestId(firstTestId).boundingBox();
|
|
const secondBox = await page.getByTestId(secondTestId).first().boundingBox();
|
|
|
|
if (!firstBox || !secondBox) {
|
|
throw new Error(`Could not measure ${firstTestId} against ${secondTestId}`);
|
|
}
|
|
|
|
expect(Math.abs(firstBox.x - secondBox.x)).toBeLessThanOrEqual(4);
|
|
}
|
|
|
|
async function expectIntroBalancedAroundDayDivider(
|
|
page: import("@playwright/test").Page,
|
|
introTestId: string,
|
|
) {
|
|
const introBox = await page.getByTestId(introTestId).boundingBox();
|
|
const dividerBox = await page
|
|
.getByTestId("message-timeline-day-divider")
|
|
.first()
|
|
.boundingBox();
|
|
const messageBox = await page
|
|
.getByTestId("message-row")
|
|
.first()
|
|
.boundingBox();
|
|
|
|
if (!introBox || !dividerBox || !messageBox) {
|
|
throw new Error(`Could not measure timeline spacing for ${introTestId}`);
|
|
}
|
|
|
|
const gapAboveDivider = dividerBox.y - (introBox.y + introBox.height);
|
|
const gapBelowDivider = messageBox.y - (dividerBox.y + dividerBox.height);
|
|
|
|
expect(Math.abs(gapAboveDivider - gapBelowDivider)).toBeLessThanOrEqual(1);
|
|
}
|
|
|
|
async function expectIntroActionCardLayout(
|
|
page: import("@playwright/test").Page,
|
|
actionTestId: string,
|
|
) {
|
|
const actionBox = await page.getByTestId(actionTestId).boundingBox();
|
|
const iconBox = await page.getByTestId(`${actionTestId}-icon`).boundingBox();
|
|
|
|
if (!actionBox || !iconBox) {
|
|
throw new Error(`Could not measure intro action card: ${actionTestId}`);
|
|
}
|
|
|
|
expect(actionBox.height).toBeGreaterThan(actionBox.width);
|
|
expect(Math.round(actionBox.width)).toBe(220);
|
|
expect(Math.round(iconBox.width)).toBe(48);
|
|
expect(Math.round(iconBox.height)).toBe(48);
|
|
const introIconRadius = await page
|
|
.getByTestId("message-channel-intro-icon")
|
|
.evaluate((element) => window.getComputedStyle(element).borderRadius);
|
|
const actionRadius = await page
|
|
.getByTestId(actionTestId)
|
|
.evaluate((element) => window.getComputedStyle(element).borderRadius);
|
|
expect(actionRadius).toBe(introIconRadius);
|
|
await expect(page.getByTestId(`${actionTestId}-title`)).toHaveCSS(
|
|
"white-space",
|
|
"normal",
|
|
);
|
|
await expect(page.getByTestId(`${actionTestId}-description`)).toHaveCSS(
|
|
"white-space",
|
|
"normal",
|
|
);
|
|
}
|
|
|
|
async function expectIntroActionsShareRow(
|
|
page: import("@playwright/test").Page,
|
|
actionTestIds: string[],
|
|
) {
|
|
const boxes = await Promise.all(
|
|
actionTestIds.map((testId) => page.getByTestId(testId).boundingBox()),
|
|
);
|
|
const measuredBoxes = boxes.filter(
|
|
(box): box is NonNullable<typeof box> => box !== null,
|
|
);
|
|
const [firstBox] = measuredBoxes;
|
|
if (!firstBox || measuredBoxes.length !== actionTestIds.length) {
|
|
throw new Error("Could not measure intro action row");
|
|
}
|
|
|
|
for (const box of measuredBoxes.slice(1)) {
|
|
expect(Math.abs(firstBox.y - box.y)).toBeLessThanOrEqual(1);
|
|
}
|
|
}
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await installMockBridge(page);
|
|
});
|
|
|
|
test("sidebar shows all channel types", async ({ page }) => {
|
|
await page.goto("/");
|
|
|
|
await expect(page.getByTestId("app-sidebar")).toBeVisible();
|
|
await expect(page.getByTestId("sidebar-agents-count")).toHaveCount(0);
|
|
|
|
// Streams
|
|
const streamList = page.getByTestId("stream-list");
|
|
await expect(streamList).toContainText("general");
|
|
await expect(streamList).toContainText("random");
|
|
await expect(streamList).toContainText("engineering");
|
|
await expect(streamList).toContainText("agents");
|
|
|
|
// Forums
|
|
const forumList = page.getByTestId("forum-list");
|
|
await expect(forumList).toContainText("watercooler");
|
|
await expect(forumList).toContainText("announcements");
|
|
|
|
// DMs
|
|
const dmList = page.getByTestId("dm-list");
|
|
await expect(dmList).toContainText("alice-tyler");
|
|
await expect(dmList).toContainText("bob-tyler");
|
|
});
|
|
|
|
test("shows presence in sidebar, DM header, and member list", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/");
|
|
|
|
await expect(page.getByTestId("sidebar-profile-card")).toBeVisible();
|
|
await expect(page.getByTestId("self-presence-badge")).toHaveAttribute(
|
|
"aria-label",
|
|
"Online",
|
|
);
|
|
await expect(page.getByTestId("channel-presence-alice-tyler")).toBeVisible();
|
|
|
|
await page.getByTestId("channel-alice-tyler").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("alice-tyler");
|
|
await expect(page.getByTestId("chat-presence-badge")).toContainText("Online");
|
|
|
|
await openMembersSidebar(page, "general");
|
|
await expect(
|
|
page.getByTestId(`sidebar-member-presence-${TEST_IDENTITIES.alice.pubkey}`),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByTestId(`sidebar-member-presence-${TEST_IDENTITIES.bob.pubkey}`),
|
|
).toBeVisible();
|
|
await page.keyboard.press("Escape");
|
|
await expect(page.getByTestId("members-sidebar")).not.toBeVisible();
|
|
});
|
|
|
|
test("start a new direct message from the sidebar", async ({ page }) => {
|
|
await page.goto("/");
|
|
|
|
await page.getByTestId("new-dm-trigger").click();
|
|
await expect(page.getByTestId("new-dm-dialog")).toBeVisible();
|
|
|
|
await page.getByTestId("new-dm-search").fill("charlie");
|
|
await expect(
|
|
page.getByTestId(`new-dm-result-${TEST_IDENTITIES.charlie.pubkey}`),
|
|
).toBeVisible();
|
|
await page.keyboard.press("Enter");
|
|
await expect(
|
|
page.getByTestId(`new-dm-selected-${TEST_IDENTITIES.charlie.pubkey}`),
|
|
).toBeVisible();
|
|
await expect(page.getByTestId("new-dm-search")).toHaveValue("");
|
|
await page
|
|
.getByTestId(`new-dm-selected-${TEST_IDENTITIES.charlie.pubkey}`)
|
|
.click();
|
|
await expect(
|
|
page.getByTestId(`new-dm-selected-${TEST_IDENTITIES.charlie.pubkey}`),
|
|
).not.toBeVisible();
|
|
await page.getByTestId("new-dm-search").fill("charlie");
|
|
await expect(
|
|
page.getByTestId(`new-dm-result-${TEST_IDENTITIES.charlie.pubkey}`),
|
|
).toBeVisible();
|
|
await page
|
|
.getByTestId(`new-dm-result-${TEST_IDENTITIES.charlie.pubkey}`)
|
|
.click();
|
|
await expect(page.getByTestId("new-dm-search")).toHaveValue("");
|
|
await expect(
|
|
page.getByTestId(`new-dm-selected-${TEST_IDENTITIES.charlie.pubkey}`),
|
|
).toBeVisible();
|
|
|
|
await page.keyboard.press("Enter");
|
|
|
|
await expect(page.getByTestId("dm-list")).toContainText("charlie");
|
|
await expect(page.getByTestId("chat-title")).toHaveText("charlie");
|
|
await expect(page.getByTestId("new-dm-trigger")).not.toBeFocused();
|
|
});
|
|
|
|
test("keeps direct message row add buttons hidden while opening", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/");
|
|
await page.evaluate(() => {
|
|
const testWindow = window as Window & {
|
|
__BUZZ_E2E__?: { mock?: { openDmDelayMs?: number } };
|
|
};
|
|
testWindow.__BUZZ_E2E__ ??= {};
|
|
testWindow.__BUZZ_E2E__.mock ??= {};
|
|
testWindow.__BUZZ_E2E__.mock.openDmDelayMs = 1_000;
|
|
});
|
|
|
|
await page.getByTestId("new-dm-trigger").click();
|
|
await page.getByTestId("new-dm-search").fill("charlie");
|
|
await page
|
|
.getByTestId(`new-dm-result-${TEST_IDENTITIES.charlie.pubkey}`)
|
|
.click();
|
|
await expect(
|
|
page.getByTestId(`new-dm-selected-${TEST_IDENTITIES.charlie.pubkey}`),
|
|
).toBeVisible();
|
|
|
|
const rowAddButtons = page.locator("[data-testid^='new-dm-add-']");
|
|
await expect(rowAddButtons.first()).toBeAttached();
|
|
|
|
await page.getByTestId("new-dm-submit").click();
|
|
|
|
await expect(page.getByTestId("new-dm-submit")).toContainText("Opening...");
|
|
await expect(
|
|
page.locator("button[data-testid^='new-dm-add-']:visible"),
|
|
).toHaveCount(0);
|
|
|
|
await expect(page.getByTestId("chat-title")).toHaveText("charlie");
|
|
});
|
|
|
|
test("shows capped participant stack in group direct message header", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/");
|
|
|
|
await page.getByTestId("new-dm-trigger").click();
|
|
await expect(page.getByTestId("new-dm-dialog")).toBeVisible();
|
|
|
|
for (const identity of [
|
|
TEST_IDENTITIES.alice,
|
|
TEST_IDENTITIES.bob,
|
|
TEST_IDENTITIES.charlie,
|
|
TEST_IDENTITIES.outsider,
|
|
]) {
|
|
await page.getByTestId("new-dm-search").fill(identity.username);
|
|
await page.getByTestId(`new-dm-result-${identity.pubkey}`).click();
|
|
}
|
|
|
|
await page.getByTestId("new-dm-submit").click();
|
|
|
|
await expect(page.getByTestId("channel-dm-count-Group DM (5)")).toHaveText(
|
|
"4",
|
|
);
|
|
await expect(page.getByTestId("chat-title")).toContainText("alice");
|
|
await expect(page.getByTestId("chat-title")).toContainText("bob");
|
|
await expect(page.getByTestId("chat-title")).toContainText("charlie");
|
|
await expect(page.getByTestId("chat-title")).toContainText("+1 more");
|
|
await expect(page.getByTestId("chat-title")).not.toContainText("outsider");
|
|
const chatTitle = await page.getByTestId("chat-title").innerText();
|
|
await expect(
|
|
page.getByTestId("message-input").locator("[data-placeholder]").first(),
|
|
).toHaveAttribute("data-placeholder", `Message ${chatTitle}`);
|
|
const composerColors = await page
|
|
.getByTestId("message-input")
|
|
.evaluate((element) => {
|
|
const placeholderElement =
|
|
element.querySelector<HTMLElement>("[data-placeholder]");
|
|
if (!placeholderElement) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
placeholderColor: window.getComputedStyle(
|
|
placeholderElement,
|
|
"::before",
|
|
).color,
|
|
textColor: window.getComputedStyle(element).color,
|
|
};
|
|
});
|
|
expect(composerColors).not.toBeNull();
|
|
expect(composerColors?.placeholderColor).not.toBe(composerColors?.textColor);
|
|
await expect(page.getByTestId("chat-header-dm-avatar")).toHaveCount(0);
|
|
await expect(page.getByTestId("chat-header-dm-avatar-stack")).toBeVisible();
|
|
await expect(page.getByTestId("chat-presence-badge")).toHaveCount(0);
|
|
await expect(
|
|
page.getByTestId("chat-header-dm-avatar-stack-participant"),
|
|
).toHaveCount(3);
|
|
await expect(page.getByTestId("chat-header-dm-avatar-stack-more")).toHaveText(
|
|
"+1",
|
|
);
|
|
const headerStackBox = await page
|
|
.getByTestId("chat-header-dm-avatar-stack")
|
|
.boundingBox();
|
|
const headerTitleBox = await page.getByTestId("chat-title").boundingBox();
|
|
expect(headerStackBox).not.toBeNull();
|
|
expect(headerTitleBox).not.toBeNull();
|
|
expect((headerStackBox?.x ?? 0) + (headerStackBox?.width ?? 0)).toBeLessThan(
|
|
headerTitleBox?.x ?? 0,
|
|
);
|
|
await expect(page.getByTestId("message-dm-intro")).toContainText("alice");
|
|
await expect(page.getByTestId("message-dm-intro")).toContainText("bob");
|
|
await expect(page.getByTestId("message-dm-intro")).toContainText("charlie");
|
|
await expect(page.getByTestId("message-dm-intro")).toContainText("+1 more");
|
|
await expect(page.getByTestId("message-dm-intro")).not.toContainText(
|
|
"outsider",
|
|
);
|
|
await expect(page.getByTestId("message-dm-intro-avatar-stack")).toBeVisible();
|
|
await expect(
|
|
page.getByTestId("message-dm-intro-avatar-stack-participant"),
|
|
).toHaveCount(3);
|
|
await expect(
|
|
page.getByTestId("message-dm-intro-avatar-stack-more"),
|
|
).toHaveText("+1");
|
|
});
|
|
|
|
test("create stream with name and description", async ({ page }) => {
|
|
const channelName = `my-new-stream-${Date.now()}`;
|
|
|
|
await page.goto("/");
|
|
await page.getByRole("button", { name: "Create a channel" }).click();
|
|
await page.getByTestId("create-channel-name").fill(channelName);
|
|
await page
|
|
.getByTestId("create-channel-description")
|
|
.fill("A stream for testing channel creation");
|
|
await page.getByTestId("create-channel-submit").click();
|
|
|
|
await expect(page.getByTestId("stream-list")).toContainText(channelName);
|
|
await expect(page.getByTestId("chat-title")).toHaveText(channelName);
|
|
});
|
|
|
|
test("create ephemeral stream shows sidebar and header affordances", async ({
|
|
page,
|
|
}) => {
|
|
const channelName = `ephemeral-stream-${Date.now()}`;
|
|
|
|
await page.goto("/");
|
|
await page.getByRole("button", { name: "Create a channel" }).click();
|
|
await page.getByTestId("create-channel-name").fill(channelName);
|
|
await page
|
|
.getByTestId("create-channel-description")
|
|
.fill("Auto-cleaned test stream");
|
|
await page.getByRole("button", { name: "Channel duration: Ongoing" }).click();
|
|
await page
|
|
.getByLabel("Ephemeral - auto-archives after 7 days of inactivity")
|
|
.click();
|
|
await page.getByTestId("create-channel-submit").click();
|
|
|
|
await expect(page.getByTestId("stream-list")).toContainText(channelName);
|
|
await expect(page.getByTestId("chat-title")).toContainText(channelName);
|
|
await expect(
|
|
page.getByTestId(`channel-ephemeral-${channelName}`),
|
|
).toBeVisible();
|
|
await expect(page.getByTestId("chat-ephemeral-badge")).toBeVisible();
|
|
await expect(page.getByTestId("chat-ephemeral-badge")).toHaveAttribute(
|
|
"aria-label",
|
|
/Ephemeral channel\. Cleans up in 7 days\./,
|
|
);
|
|
|
|
await page
|
|
.getByRole("button", { name: "Toggle Sidebar", exact: true })
|
|
.click();
|
|
await expect(
|
|
page.getByTestId(`channel-ephemeral-${channelName}`),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test("ephemeral countdown refreshes when switching channels after a clock jump", async ({
|
|
page,
|
|
}) => {
|
|
const firstChannelName = "ephemeral-alpha";
|
|
const secondChannelName = "ephemeral-beta";
|
|
const initialTime = new Date("2026-04-09T00:00:00.000Z");
|
|
const shiftedTime = new Date("2026-04-15T02:00:00.000Z");
|
|
|
|
await page.clock.setFixedTime(initialTime);
|
|
await page.goto("/");
|
|
|
|
for (const channelName of [firstChannelName, secondChannelName]) {
|
|
await page.getByRole("button", { name: "Create a channel" }).click();
|
|
await page.getByTestId("create-channel-name").fill(channelName);
|
|
await page
|
|
.getByTestId("create-channel-description")
|
|
.fill("Auto-cleaned test stream");
|
|
await page
|
|
.getByRole("button", { name: "Channel duration: Ongoing" })
|
|
.click();
|
|
await page
|
|
.getByLabel("Ephemeral - auto-archives after 7 days of inactivity")
|
|
.click();
|
|
await page.getByTestId("create-channel-submit").click();
|
|
await expect(page.getByTestId("chat-title")).toContainText(channelName);
|
|
}
|
|
|
|
await page.clock.setFixedTime(shiftedTime);
|
|
await expect
|
|
.poll(() => page.evaluate(() => Date.now()))
|
|
.toBe(shiftedTime.getTime());
|
|
|
|
await page.getByTestId(`channel-${firstChannelName}`).click();
|
|
await expect(page.getByTestId("chat-title")).toContainText(firstChannelName);
|
|
await expect(page.getByTestId("chat-ephemeral-badge")).toHaveAttribute(
|
|
"aria-label",
|
|
/Ephemeral channel\. Cleans up in 22 hours\./,
|
|
);
|
|
});
|
|
|
|
test("archived channels stay out of all sidebar sections", async ({ page }) => {
|
|
const archivedStreamName = `archived-stream-${Date.now()}`;
|
|
const archivedForumName = `archived-forum-${Date.now()}`;
|
|
|
|
await page.goto("/");
|
|
await page.waitForFunction(() => {
|
|
return Boolean(
|
|
(
|
|
window as Window & {
|
|
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: unknown;
|
|
}
|
|
).__BUZZ_E2E_INVOKE_MOCK_COMMAND__,
|
|
);
|
|
});
|
|
await page.evaluate(
|
|
async ({
|
|
archivedForumName,
|
|
archivedStreamName,
|
|
outsiderPubkey,
|
|
}: {
|
|
archivedForumName: string;
|
|
archivedStreamName: string;
|
|
outsiderPubkey: string;
|
|
}) => {
|
|
const invoke = (
|
|
window as Window & {
|
|
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
|
|
command: string,
|
|
payload?: Record<string, unknown>,
|
|
) => Promise<{ id: string }>;
|
|
}
|
|
).__BUZZ_E2E_INVOKE_MOCK_COMMAND__;
|
|
|
|
if (!invoke) {
|
|
throw new Error("Mock bridge is not installed.");
|
|
}
|
|
|
|
const stream = await invoke("create_channel", {
|
|
channelType: "stream",
|
|
name: archivedStreamName,
|
|
visibility: "open",
|
|
});
|
|
const forum = await invoke("create_channel", {
|
|
channelType: "forum",
|
|
name: archivedForumName,
|
|
visibility: "open",
|
|
});
|
|
const directMessage = await invoke("open_dm", {
|
|
pubkeys: [outsiderPubkey],
|
|
});
|
|
|
|
for (const channel of [stream, forum, directMessage]) {
|
|
await invoke("archive_channel", { channelId: channel.id });
|
|
}
|
|
},
|
|
{
|
|
archivedForumName,
|
|
archivedStreamName,
|
|
outsiderPubkey: TEST_IDENTITIES.outsider.pubkey,
|
|
},
|
|
);
|
|
|
|
await page.reload();
|
|
|
|
await expect(page.getByTestId("stream-list")).not.toContainText(
|
|
archivedStreamName,
|
|
);
|
|
await expect(page.getByTestId("forum-list")).not.toContainText(
|
|
archivedForumName,
|
|
);
|
|
await expect(page.getByTestId("dm-list")).toContainText("alice-tyler");
|
|
await expect(page.getByTestId("dm-list")).not.toContainText("outsider");
|
|
});
|
|
|
|
test("create stream with special characters", async ({ page }) => {
|
|
const channelName = `dev ops-${Date.now()}`;
|
|
|
|
await page.goto("/");
|
|
await page.getByRole("button", { name: "Create a channel" }).click();
|
|
await page.getByTestId("create-channel-name").fill(channelName);
|
|
await page
|
|
.getByTestId("create-channel-description")
|
|
.fill("Stream with spaces and hyphens");
|
|
await page.getByTestId("create-channel-submit").click();
|
|
|
|
await expect(page.getByTestId("stream-list")).toContainText(channelName);
|
|
await expect(page.getByTestId("chat-title")).toHaveText(channelName);
|
|
});
|
|
|
|
test("switch between streams", async ({ page }) => {
|
|
await page.goto("/");
|
|
|
|
await page.getByTestId("channel-general").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
|
|
|
await page.getByTestId("channel-random").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("random");
|
|
|
|
await page.getByTestId("channel-engineering").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("engineering");
|
|
});
|
|
|
|
test("switch between channel types", async ({ page }) => {
|
|
await page.goto("/");
|
|
|
|
// Stream
|
|
await page.getByTestId("channel-general").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
|
|
|
// Forum
|
|
await page.getByTestId("channel-watercooler").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("watercooler");
|
|
|
|
// DM
|
|
await page.getByTestId("channel-alice-tyler").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("alice-tyler");
|
|
});
|
|
|
|
test("empty channel shows intro actions", async ({ page }) => {
|
|
await page.goto("/");
|
|
|
|
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("channel-intro-action-create-channel"),
|
|
).toHaveCount(0);
|
|
await expect(
|
|
page.getByTestId("channel-intro-action-create-agent"),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByTestId("channel-intro-action-add-people"),
|
|
).toBeVisible();
|
|
await expect(page.getByTestId("welcome-composer-guide-banner")).toHaveCount(
|
|
0,
|
|
);
|
|
await expectIntroActionCardLayout(page, "channel-intro-action-create-agent");
|
|
await expectIntroActionsShareRow(page, [
|
|
"channel-intro-action-create-agent",
|
|
"channel-intro-action-add-people",
|
|
]);
|
|
|
|
await page.getByTestId("channel-intro-action-add-people").click();
|
|
await expect(page.getByTestId("members-sidebar")).toBeVisible();
|
|
|
|
await page.keyboard.press("Escape");
|
|
await expect(page.getByTestId("members-sidebar")).not.toBeVisible();
|
|
|
|
await page.getByTestId("channel-intro-action-create-agent").click();
|
|
await expect(page.getByRole("heading", { name: "Add agents" })).toBeVisible();
|
|
|
|
await page.keyboard.press("Escape");
|
|
await expect(page.getByRole("heading", { name: "Add agents" })).toHaveCount(
|
|
0,
|
|
);
|
|
});
|
|
|
|
test("short channel with messages shows intro actions on open", async ({
|
|
page,
|
|
}) => {
|
|
const channelName = `short-intro-${Date.now()}`;
|
|
const message = "Only message in a short channel";
|
|
|
|
await page.goto("/");
|
|
await page.getByRole("button", { name: "Create a channel" }).click();
|
|
await page.getByTestId("create-channel-name").fill(channelName);
|
|
await page.getByTestId("create-channel-submit").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText(channelName);
|
|
|
|
await page.getByTestId("message-input").fill(message);
|
|
await page.getByTestId("send-message").click();
|
|
await expect(page.getByTestId("message-timeline")).toContainText(message);
|
|
|
|
await page.getByTestId("channel-general").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
|
await page.getByTestId(`channel-${channelName}`).click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText(channelName);
|
|
await expect(page.getByTestId("message-timeline")).toContainText(message);
|
|
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("channel-intro-action-create-agent"),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByTestId("channel-intro-action-add-people"),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test("scrollable channel with recent messages hides intro actions until top", async ({
|
|
page,
|
|
}) => {
|
|
const channelName = `long-intro-${Date.now()}`;
|
|
const messages = Array.from(
|
|
{ length: 24 },
|
|
(_, index) => `Scrollable channel message ${index + 1}`,
|
|
);
|
|
|
|
await page.goto("/");
|
|
await page.getByRole("button", { name: "Create a channel" }).click();
|
|
await page.getByTestId("create-channel-name").fill(channelName);
|
|
await page.getByTestId("create-channel-submit").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText(channelName);
|
|
|
|
for (const message of messages) {
|
|
await page.getByTestId("message-input").fill(message);
|
|
await page.getByTestId("send-message").click();
|
|
await expect(page.getByTestId("message-timeline")).toContainText(message);
|
|
}
|
|
|
|
await page.getByTestId("channel-general").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
|
await page.getByTestId(`channel-${channelName}`).click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText(channelName);
|
|
await expect(page.getByTestId("message-channel-intro")).toHaveCount(0);
|
|
await expect(
|
|
page.getByTestId("channel-intro-action-create-agent"),
|
|
).toHaveCount(0);
|
|
await expect(page.getByTestId("channel-intro-action-add-people")).toHaveCount(
|
|
0,
|
|
);
|
|
await expect(page.getByTestId("message-timeline")).toContainText(
|
|
messages[messages.length - 1],
|
|
);
|
|
|
|
await page.getByTestId("message-timeline").evaluate((element) => {
|
|
element.scrollTop = 0;
|
|
element.dispatchEvent(new Event("scroll", { bubbles: true }));
|
|
});
|
|
await expect(page.getByTestId("message-channel-intro")).toBeVisible();
|
|
await expect(
|
|
page.getByTestId("channel-intro-action-create-agent"),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByTestId("channel-intro-action-add-people"),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test("channel with messages shows content", async ({ page }) => {
|
|
await page.goto("/");
|
|
|
|
await page.getByTestId("channel-general").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
|
await expect(page.getByTestId("message-channel-intro")).toBeVisible();
|
|
await expect(
|
|
page.getByTestId("channel-intro-action-create-channel"),
|
|
).toHaveCount(0);
|
|
await expect(page.getByTestId("welcome-composer-guide-banner")).toHaveCount(
|
|
0,
|
|
);
|
|
await expect(page.getByTestId("message-timeline-day-divider")).toBeVisible();
|
|
await expect(page.getByTestId("message-timeline")).toContainText(
|
|
"Welcome to #general",
|
|
);
|
|
});
|
|
|
|
test("shows and clears activity indicators for active channel agents", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/");
|
|
|
|
await page.getByTestId("channel-agents").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("agents");
|
|
await waitForMockLiveSubscription(page, "agents", KIND_TYPING_INDICATOR);
|
|
|
|
await page.evaluate((pubkey) => {
|
|
window.__BUZZ_E2E_EMIT_MOCK_TYPING__?.({
|
|
channelName: "agents",
|
|
pubkey,
|
|
});
|
|
}, TEST_IDENTITIES.alice.pubkey);
|
|
|
|
await expect(page.getByTestId("bot-activity-composer-trigger")).toBeVisible();
|
|
await page.getByTestId("bot-activity-composer-trigger").click();
|
|
await expect(
|
|
page.getByTestId(
|
|
`bot-activity-composer-item-${TEST_IDENTITIES.alice.pubkey}`,
|
|
),
|
|
).toBeVisible();
|
|
await page
|
|
.getByTestId(`bot-activity-composer-item-${TEST_IDENTITIES.alice.pubkey}`)
|
|
.click({ force: true });
|
|
await expect(page.getByTestId("agent-session-thread-panel")).toBeVisible();
|
|
await expect(page.getByTestId("agent-session-thread-panel")).toContainText(
|
|
"alice",
|
|
);
|
|
await expect(page.getByTestId("message-typing-indicator")).toHaveCount(0);
|
|
|
|
await page.evaluate((pubkey) => {
|
|
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
|
|
channelName: "agents",
|
|
content: "Done.",
|
|
pubkey,
|
|
});
|
|
}, TEST_IDENTITIES.alice.pubkey);
|
|
|
|
await expect(page.getByTestId("message-timeline")).toContainText("Done.");
|
|
await expect(page.getByTestId("bot-activity-composer-trigger")).toHaveCount(
|
|
0,
|
|
);
|
|
|
|
await page.waitForTimeout(1_200);
|
|
await page.evaluate((pubkey) => {
|
|
window.__BUZZ_E2E_EMIT_MOCK_TYPING__?.({
|
|
channelName: "agents",
|
|
pubkey,
|
|
});
|
|
}, TEST_IDENTITIES.alice.pubkey);
|
|
|
|
await expect(page.getByTestId("bot-activity-composer-trigger")).toHaveCount(
|
|
0,
|
|
);
|
|
});
|
|
|
|
test("typing indicator shows avatars and maintains stable name order", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/");
|
|
|
|
await page.getByTestId("channel-random").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("random");
|
|
await waitForMockLiveSubscription(page, "random", KIND_TYPING_INDICATOR);
|
|
|
|
// Alice starts typing first
|
|
await page.evaluate((pubkey) => {
|
|
window.__BUZZ_E2E_EMIT_MOCK_TYPING__?.({
|
|
channelName: "random",
|
|
pubkey,
|
|
});
|
|
}, TEST_IDENTITIES.alice.pubkey);
|
|
|
|
await expect(page.getByTestId("message-typing-indicator")).toBeVisible();
|
|
await expect(
|
|
page.getByTestId("message-typing-indicator-label"),
|
|
).toContainText("alice is typing");
|
|
|
|
// Verify avatar is rendered for the typing user
|
|
const avatars = page
|
|
.getByTestId("message-typing-indicator")
|
|
.locator("[data-testid='message-typing-avatar']");
|
|
await expect(avatars).toHaveCount(1);
|
|
|
|
// Bob starts typing second
|
|
await page.evaluate((pubkey) => {
|
|
window.__BUZZ_E2E_EMIT_MOCK_TYPING__?.({
|
|
channelName: "random",
|
|
pubkey,
|
|
});
|
|
}, TEST_IDENTITIES.bob.pubkey);
|
|
|
|
await expect(
|
|
page.getByTestId("message-typing-indicator-label"),
|
|
).toContainText("alice and bob are typing");
|
|
await expect(avatars).toHaveCount(2);
|
|
|
|
// Alice re-broadcasts — order should stay "alice and bob", not flip
|
|
await page.evaluate((pubkey) => {
|
|
window.__BUZZ_E2E_EMIT_MOCK_TYPING__?.({
|
|
channelName: "random",
|
|
pubkey,
|
|
});
|
|
}, TEST_IDENTITIES.alice.pubkey);
|
|
|
|
await expect(
|
|
page.getByTestId("message-typing-indicator-label"),
|
|
).toContainText("alice and bob are typing");
|
|
|
|
// Bob re-broadcasts — order should still stay "alice and bob"
|
|
await page.evaluate((pubkey) => {
|
|
window.__BUZZ_E2E_EMIT_MOCK_TYPING__?.({
|
|
channelName: "random",
|
|
pubkey,
|
|
});
|
|
}, TEST_IDENTITIES.bob.pubkey);
|
|
|
|
await expect(
|
|
page.getByTestId("message-typing-indicator-label"),
|
|
).toContainText("alice and bob are typing");
|
|
});
|
|
|
|
test("sidebar shows unread indicator for newly active channels", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/");
|
|
|
|
await expect(page.getByTestId("channel-unread-random")).toHaveCount(0);
|
|
await waitForMockLiveSubscription(page, "random");
|
|
|
|
// The unread tracker ignores the current user's own messages, so emit as
|
|
// alice — simulating a real "another user posted while I was elsewhere".
|
|
await page.evaluate(
|
|
({ pubkey }) => {
|
|
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
|
|
channelName: "random",
|
|
content: "Unread update for #random",
|
|
kind: 40002,
|
|
pubkey,
|
|
});
|
|
},
|
|
{ pubkey: TEST_IDENTITIES.alice.pubkey },
|
|
);
|
|
|
|
await expect(page.getByTestId("channel-unread-random")).toBeVisible();
|
|
|
|
await page.getByTestId("channel-random").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("random");
|
|
await expect(page.getByTestId("message-timeline")).toContainText(
|
|
"Unread update for #random",
|
|
);
|
|
await expect(page.getByTestId("channel-unread-random")).toHaveCount(0);
|
|
});
|
|
|
|
test("sidebar shows unread indicator for new forum posts", async ({ page }) => {
|
|
await page.goto("/");
|
|
|
|
await expect(page.getByTestId("channel-unread-watercooler")).toHaveCount(0);
|
|
await waitForMockLiveSubscription(page, "watercooler");
|
|
|
|
// Emit as alice — the unread tracker ignores self-authored messages.
|
|
await page.evaluate(
|
|
({ pubkey }) => {
|
|
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
|
|
channelName: "watercooler",
|
|
content: "Unread update for the forum",
|
|
kind: 45001,
|
|
pubkey,
|
|
});
|
|
},
|
|
{ pubkey: TEST_IDENTITIES.alice.pubkey },
|
|
);
|
|
|
|
await expect(page.getByTestId("channel-unread-watercooler")).toBeVisible();
|
|
|
|
await page.getByTestId("channel-watercooler").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("watercooler");
|
|
await expect(page.getByTestId("channel-unread-watercooler")).toHaveCount(0);
|
|
});
|
|
|
|
test("sidebar clears unread indicator after opening a DM", async ({ page }) => {
|
|
await page.goto("/");
|
|
|
|
await expect(page.getByTestId("channel-unread-alice-tyler")).toHaveCount(0);
|
|
await waitForMockLiveSubscription(page, "alice-tyler");
|
|
|
|
await page.evaluate((pubkey) => {
|
|
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
|
|
channelName: "alice-tyler",
|
|
content: "Unread update for the DM",
|
|
pubkey,
|
|
});
|
|
}, TEST_IDENTITIES.alice.pubkey);
|
|
|
|
await expect(page.getByTestId("channel-unread-alice-tyler")).toBeVisible();
|
|
|
|
await page.getByTestId("channel-alice-tyler").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("alice-tyler");
|
|
await expect(page.getByTestId("message-dm-intro")).toBeVisible();
|
|
await expect(page.getByTestId("message-dm-intro")).toContainText(
|
|
"This is the beginning of your direct message with",
|
|
);
|
|
await expect(page.getByTestId("message-timeline-day-divider")).toBeVisible();
|
|
await expect(page.getByTestId("message-timeline")).toContainText(
|
|
"Unread update for the DM",
|
|
);
|
|
await expectSameLeftInset(page, "message-dm-intro", "message-row");
|
|
await expectIntroBalancedAroundDayDivider(page, "message-dm-intro");
|
|
await expect(page.getByTestId("channel-unread-alice-tyler")).toHaveCount(0);
|
|
});
|
|
|
|
test("sidebar persists after channel switch", async ({ page }) => {
|
|
await page.goto("/");
|
|
|
|
await expect(page.getByTestId("app-sidebar")).toBeVisible();
|
|
|
|
await page.getByTestId("channel-general").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
|
await expect(page.getByTestId("app-sidebar")).toBeVisible();
|
|
|
|
await page.getByTestId("channel-random").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("random");
|
|
await expect(page.getByTestId("app-sidebar")).toBeVisible();
|
|
|
|
await page.getByTestId("channel-watercooler").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("watercooler");
|
|
await expect(page.getByTestId("app-sidebar")).toBeVisible();
|
|
});
|
|
|
|
test("manage channel updates details and context", async ({ page }) => {
|
|
const stamp = Date.now();
|
|
const newName = `release-hub-${stamp}`;
|
|
const newDescription = `Release coordination ${stamp}`;
|
|
const newTopic = `Launch plan ${stamp}`;
|
|
const newPurpose = `Track blockers and owners ${stamp}`;
|
|
|
|
await page.goto("/");
|
|
await openChannelManagement(page, "general");
|
|
|
|
await page.getByTestId("channel-management-name").fill(newName);
|
|
await page.getByTestId("channel-management-description").fill(newDescription);
|
|
await page.getByTestId("channel-management-save-details").click();
|
|
|
|
await expect(page.getByTestId("chat-title")).toHaveText(newName);
|
|
await expect(page.getByTestId("stream-list")).toContainText(newName);
|
|
|
|
const saveTopicButton = page.getByTestId("channel-management-save-topic");
|
|
const savePurposeButton = page.getByTestId("channel-management-save-purpose");
|
|
|
|
await page.getByTestId("channel-management-topic").fill(newTopic);
|
|
await saveTopicButton.click();
|
|
await expect(saveTopicButton).toHaveText("Save topic");
|
|
await expect(page.getByTestId("channel-management-topic")).toHaveValue(
|
|
newTopic,
|
|
);
|
|
|
|
await page.getByTestId("channel-management-purpose").fill(newPurpose);
|
|
await savePurposeButton.click();
|
|
await expect(savePurposeButton).toHaveText("Save purpose");
|
|
await expect(page.getByTestId("channel-management-purpose")).toHaveValue(
|
|
newPurpose,
|
|
);
|
|
|
|
await closeChannelManagement(page);
|
|
|
|
await page.getByTestId("channel-random").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("random");
|
|
|
|
await page.getByTestId("stream-list").getByText(newName).click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText(newName);
|
|
await page.getByTestId("channel-management-trigger").click();
|
|
await expect(page.getByTestId("channel-management-sheet")).toBeVisible();
|
|
|
|
await expect(page.getByTestId("channel-management-name")).toHaveValue(
|
|
newName,
|
|
);
|
|
await expect(page.getByTestId("channel-management-description")).toHaveValue(
|
|
newDescription,
|
|
);
|
|
await expect(page.getByTestId("channel-management-topic")).toHaveValue(
|
|
newTopic,
|
|
);
|
|
await expect(page.getByTestId("channel-management-purpose")).toHaveValue(
|
|
newPurpose,
|
|
);
|
|
});
|
|
|
|
test("manage channel updates visibility and ephemeral lifecycle independently", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/");
|
|
await openChannelManagement(page, "general");
|
|
|
|
const saveDetailsButton = page.getByTestId("channel-management-save-details");
|
|
const saveLifecycleButton = page.getByTestId(
|
|
"channel-management-save-lifecycle",
|
|
);
|
|
|
|
await expect(saveLifecycleButton).toBeDisabled();
|
|
|
|
await page.getByTestId("channel-management-private-toggle").click();
|
|
await page.getByTestId("channel-management-ephemeral-toggle").click();
|
|
await expect(page.getByTestId("channel-management-ttl")).toBeVisible();
|
|
await expect(saveLifecycleButton).toBeEnabled();
|
|
|
|
const commandCountBeforeEnable = (await readCommandPayloadLog(page)).length;
|
|
await saveLifecycleButton.click();
|
|
await expect
|
|
.poll(async () =>
|
|
(await readCommandPayloadLog(page)).slice(commandCountBeforeEnable),
|
|
)
|
|
.toContainEqual(
|
|
expect.objectContaining({
|
|
command: "update_channel",
|
|
payload: expect.objectContaining({
|
|
input: expect.objectContaining({ ttlSeconds: 604800 }),
|
|
}),
|
|
}),
|
|
);
|
|
await expect(saveLifecycleButton).toHaveText("Save visibility");
|
|
await expect(saveDetailsButton).toHaveText("Save details");
|
|
|
|
const channelAfterEnable = await invokeMockCommand<{
|
|
ttl_seconds: number | null;
|
|
visibility: string;
|
|
}>(page, "get_channel_details", {
|
|
channelId: "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50",
|
|
});
|
|
expect(channelAfterEnable).toMatchObject({
|
|
ttl_seconds: 604800,
|
|
visibility: "private",
|
|
});
|
|
|
|
await closeChannelManagement(page);
|
|
await openChannelManagement(page, "general");
|
|
|
|
await expect(
|
|
page.getByTestId("channel-management-private-toggle"),
|
|
).toHaveAttribute("data-state", "checked");
|
|
await expect(
|
|
page.getByTestId("channel-management-ephemeral-toggle"),
|
|
).toHaveAttribute("data-state", "checked");
|
|
await expect(page.getByTestId("channel-management-ttl")).toHaveValue("7d");
|
|
|
|
await page.getByTestId("channel-management-private-toggle").click();
|
|
await page.getByTestId("channel-management-ephemeral-toggle").click();
|
|
await expect(saveLifecycleButton).toBeEnabled();
|
|
|
|
const commandCountBeforeDisable = (await readCommandPayloadLog(page)).length;
|
|
await saveLifecycleButton.click();
|
|
await expect
|
|
.poll(async () =>
|
|
(await readCommandPayloadLog(page)).slice(commandCountBeforeDisable),
|
|
)
|
|
.toContainEqual(
|
|
expect.objectContaining({
|
|
command: "update_channel",
|
|
payload: expect.objectContaining({
|
|
input: expect.objectContaining({ ttlSeconds: null }),
|
|
}),
|
|
}),
|
|
);
|
|
await expect(saveLifecycleButton).toHaveText("Save visibility");
|
|
await expect(saveDetailsButton).toHaveText("Save details");
|
|
await expect(page.getByTestId("channel-management-ttl")).toHaveCount(0);
|
|
|
|
const channelAfterDisable = await invokeMockCommand<{
|
|
ttl_seconds: number | null;
|
|
visibility: string;
|
|
}>(page, "get_channel_details", {
|
|
channelId: "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50",
|
|
});
|
|
expect(channelAfterDisable).toMatchObject({
|
|
ttl_seconds: null,
|
|
visibility: "open",
|
|
});
|
|
|
|
await closeChannelManagement(page);
|
|
await openChannelManagement(page, "general");
|
|
|
|
await expect(
|
|
page.getByTestId("channel-management-private-toggle"),
|
|
).toHaveAttribute("data-state", "unchecked");
|
|
await expect(
|
|
page.getByTestId("channel-management-ephemeral-toggle"),
|
|
).toHaveAttribute("data-state", "unchecked");
|
|
await expect(page.getByTestId("channel-management-ttl")).toHaveCount(0);
|
|
});
|
|
|
|
test("manage channel keeps canvas near the top of the sheet", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/");
|
|
await openChannelManagement(page, "general");
|
|
|
|
const sheet = page.getByTestId("channel-management-sheet");
|
|
|
|
// Canvas section should appear before the name input in the DOM.
|
|
const canvasBox = await sheet
|
|
.getByTestId("channel-canvas-section")
|
|
.boundingBox();
|
|
const nameBox = await sheet
|
|
.getByTestId("channel-management-name")
|
|
.boundingBox();
|
|
|
|
expect(canvasBox).not.toBeNull();
|
|
expect(nameBox).not.toBeNull();
|
|
expect(canvasBox?.y).toBeLessThan(nameBox?.y);
|
|
});
|
|
|
|
test("members sidebar can invite and remove members", async ({ page }) => {
|
|
await page.goto("/");
|
|
await openMembersSidebar(page, "general");
|
|
const initialMemberCount = await readMembersTriggerCount(page);
|
|
await expect(page.getByTestId("channel-management-add-pubkeys")).toHaveCount(
|
|
0,
|
|
);
|
|
|
|
await expect(page.getByText(/Members · \d+/)).toBeVisible();
|
|
await page.getByTestId("channel-management-search-users").fill("a");
|
|
await expect(page.getByText("Members", { exact: true })).toBeVisible();
|
|
await expect(page.getByText(/Members · \d+/)).toHaveCount(0);
|
|
await expect(
|
|
page.getByText("Not in this channel", { exact: true }),
|
|
).toBeVisible();
|
|
|
|
await page.getByTestId("channel-management-search-users").fill("char");
|
|
await expect(
|
|
page.getByTestId(
|
|
`channel-user-search-result-${TEST_IDENTITIES.charlie.pubkey}`,
|
|
),
|
|
).toBeVisible();
|
|
await page
|
|
.getByTestId(`channel-user-search-result-${TEST_IDENTITIES.charlie.pubkey}`)
|
|
.click();
|
|
await expect(
|
|
page.getByTestId(`sidebar-member-${TEST_IDENTITIES.charlie.pubkey}`),
|
|
).toContainText("charlie");
|
|
await expect(
|
|
page.getByTestId(`selected-invitee-${TEST_IDENTITIES.charlie.pubkey}`),
|
|
).toHaveCount(0);
|
|
await expect(page.getByTestId("channel-management-add-members")).toHaveCount(
|
|
0,
|
|
);
|
|
await expect(page.getByTestId("channel-management-search-users")).toHaveValue(
|
|
"char",
|
|
);
|
|
await expectMembersTriggerCount(page, initialMemberCount + 1);
|
|
|
|
await openMemberMenu(page, TEST_IDENTITIES.charlie.pubkey);
|
|
await page
|
|
.getByTestId(`sidebar-remove-member-${TEST_IDENTITIES.charlie.pubkey}`)
|
|
.click();
|
|
|
|
await expect(
|
|
page.getByTestId(`sidebar-member-${TEST_IDENTITIES.charlie.pubkey}`),
|
|
).toHaveCount(0);
|
|
await expectMembersTriggerCount(page, initialMemberCount);
|
|
});
|
|
|
|
test("members modal does not show direct pubkey entry", async ({ page }) => {
|
|
await page.goto("/");
|
|
await openMembersSidebar(page, "general");
|
|
|
|
await expect(page.getByTestId("channel-management-add-pubkeys")).toHaveCount(
|
|
0,
|
|
);
|
|
await expect(
|
|
page.getByTestId("channel-management-toggle-direct-pubkeys"),
|
|
).toHaveCount(0);
|
|
await expect(
|
|
page.getByTestId("channel-management-search-users"),
|
|
).toHaveAttribute("placeholder", "Add people and agents");
|
|
});
|
|
|
|
test("channel header omits the add agent action", async ({ page }) => {
|
|
await page.goto("/");
|
|
|
|
await page.getByTestId("channel-random").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("random");
|
|
await page.setViewportSize({ width: 1280, height: 420 });
|
|
|
|
await expect(page.getByTestId("channel-add-bot-trigger")).toHaveCount(0);
|
|
await expect(page.getByTestId("channel-members-trigger")).toBeVisible();
|
|
await expect(page.getByTestId("channel-start-huddle-trigger")).toBeVisible();
|
|
await expect(page.getByTestId("channel-management-trigger")).toBeVisible();
|
|
});
|
|
|
|
test("private-channel members can add members and bots without admin", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/");
|
|
// secret-projects is a private (non-DM) channel where the current user is a
|
|
// plain member, not owner/admin. They should still be able to add members
|
|
// and bots — only granting elevated roles is reserved for owners/admins.
|
|
await openMembersSidebar(page, "secret-projects");
|
|
|
|
// The invite card is shown to any member, not just owners/admins.
|
|
await expect(
|
|
page.getByTestId("channel-management-search-users"),
|
|
).toBeVisible();
|
|
await page.getByTestId("channel-management-search-users").fill("char");
|
|
await page
|
|
.getByTestId(`channel-user-search-result-${TEST_IDENTITIES.charlie.pubkey}`)
|
|
.click();
|
|
await expect(
|
|
page.getByTestId(`sidebar-member-${TEST_IDENTITIES.charlie.pubkey}`),
|
|
).toContainText("charlie");
|
|
|
|
// The modal no longer exposes member/guest/bot role choices or a staged
|
|
// submit button; selected people are added immediately as members and
|
|
// selected agents are added as bots.
|
|
await expect(page.getByTestId("channel-management-add-role")).toHaveCount(0);
|
|
await expect(page.getByTestId("channel-management-add-members")).toHaveCount(
|
|
0,
|
|
);
|
|
});
|
|
|
|
test("removing a channel-scoped agent preserves the managed agent record", async ({
|
|
page,
|
|
}) => {
|
|
const agentName = `cleanup-agent-${Date.now()}`;
|
|
|
|
await page.goto("/");
|
|
const agentPubkey = await addGenericAgent(page, "general", agentName);
|
|
|
|
await page.getByTestId("channel-general").click();
|
|
await openMembersSidebar(page, "general");
|
|
|
|
await openMemberMenu(page, agentPubkey);
|
|
await page.getByTestId(`sidebar-remove-member-${agentPubkey}`).click();
|
|
await expect(page.getByTestId(`sidebar-member-${agentPubkey}`)).toHaveCount(
|
|
0,
|
|
);
|
|
await page.keyboard.press("Escape");
|
|
await expect(page.getByTestId("members-sidebar")).not.toBeVisible();
|
|
|
|
await page.getByTestId("open-agents-view").click();
|
|
await expect(page.getByTestId(`managed-agent-${agentPubkey}`)).toHaveCount(1);
|
|
});
|
|
|
|
test("members sidebar can respawn a stopped managed bot", async ({ page }) => {
|
|
const agentName = `sidebar-agent-${Date.now()}`;
|
|
|
|
await page.goto("/");
|
|
const agentPubkey = await addGenericAgent(page, "general", agentName);
|
|
const baselineCommands = await readCommandLog(page);
|
|
const baselineStartCount = baselineCommands.filter(
|
|
(command) => command === "start_managed_agent",
|
|
).length;
|
|
const baselineStopCount = baselineCommands.filter(
|
|
(command) => command === "stop_managed_agent",
|
|
).length;
|
|
|
|
await openMembersSidebar(page, "general");
|
|
|
|
const agentStatus = page.getByTestId(
|
|
`sidebar-managed-agent-status-${agentPubkey}`,
|
|
);
|
|
const agentAction = page.getByTestId(`sidebar-agent-action-${agentPubkey}`);
|
|
|
|
await expect(agentStatus).toContainText("Running");
|
|
await openMemberMenu(page, agentPubkey);
|
|
await expect(agentAction).toContainText("Stop");
|
|
await agentAction.click();
|
|
|
|
await expect(agentStatus).toContainText("Stopped");
|
|
await openMemberMenu(page, agentPubkey);
|
|
await expect(agentAction).toContainText("Respawn");
|
|
await agentAction.click();
|
|
|
|
await expect(agentStatus).toContainText("Running");
|
|
await expect(
|
|
page
|
|
.locator("[data-sonner-toast]")
|
|
.filter({ hasText: `Respawned ${agentName}.` }),
|
|
).toBeVisible();
|
|
|
|
const commands = await readCommandLog(page);
|
|
expect(
|
|
commands.filter((command) => command === "start_managed_agent").length,
|
|
).toBe(baselineStartCount + 1);
|
|
expect(
|
|
commands.filter((command) => command === "stop_managed_agent").length,
|
|
).toBe(baselineStopCount + 1);
|
|
});
|
|
|
|
test("members sidebar omits bulk controls for managed bots", async ({
|
|
page,
|
|
}) => {
|
|
const firstAgentName = `sidebar-remove-a-${Date.now()}`;
|
|
const secondAgentName = `sidebar-remove-b-${Date.now()}`;
|
|
|
|
await page.goto("/");
|
|
const firstAgentPubkey = await addGenericAgent(
|
|
page,
|
|
"general",
|
|
firstAgentName,
|
|
);
|
|
const secondAgentPubkey = await addGenericAgent(
|
|
page,
|
|
"general",
|
|
secondAgentName,
|
|
);
|
|
|
|
await openMembersSidebar(page, "general");
|
|
await expect(
|
|
page.getByTestId(`sidebar-member-${firstAgentPubkey}`),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByTestId(`sidebar-member-${secondAgentPubkey}`),
|
|
).toBeVisible();
|
|
await expect(page.getByTestId("members-sidebar-agent-controls")).toHaveCount(
|
|
0,
|
|
);
|
|
await expect(page.getByTestId("members-sidebar-remove-all")).toHaveCount(0);
|
|
|
|
await page.keyboard.press("Escape");
|
|
await expect(page.getByTestId("members-sidebar")).not.toBeVisible();
|
|
|
|
await page.getByTestId("open-agents-view").click();
|
|
await expect(
|
|
page.getByTestId(`managed-agent-${firstAgentPubkey}`),
|
|
).toHaveCount(1);
|
|
await expect(
|
|
page.getByTestId(`managed-agent-${secondAgentPubkey}`),
|
|
).toHaveCount(1);
|
|
|
|
const commands = await readCommandLog(page);
|
|
expect(
|
|
commands.filter((command) => command === "remove_channel_member"),
|
|
).toHaveLength(0);
|
|
});
|
|
|
|
test("removing a multi-channel managed bot preserves its record after removal from all channels", async ({
|
|
page,
|
|
}) => {
|
|
const agentName = `multi-channel-agent-${Date.now()}`;
|
|
const secondChannelName = `multi-home-${Date.now()}`;
|
|
|
|
await page.goto("/");
|
|
const agentPubkey = await addGenericAgent(page, "general", agentName);
|
|
|
|
await page.getByRole("button", { name: "Create a channel" }).click();
|
|
await page.getByTestId("create-channel-name").fill(secondChannelName);
|
|
await page
|
|
.getByTestId("create-channel-description")
|
|
.fill("Second home for managed bot cleanup coverage");
|
|
await page.getByTestId("create-channel-submit").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText(secondChannelName);
|
|
|
|
const secondChannelId = await page
|
|
.getByTestId(`channel-${secondChannelName}`)
|
|
.getAttribute("data-channel-id");
|
|
if (!secondChannelId) {
|
|
throw new Error("Second channel id missing.");
|
|
}
|
|
|
|
await invokeMockCommand(page, "add_channel_members", {
|
|
channelId: secondChannelId,
|
|
pubkeys: [agentPubkey],
|
|
role: "bot",
|
|
});
|
|
|
|
// Snapshot command counts before removals so assertions are relative.
|
|
const baseline = await readCommandLog(page);
|
|
const baselineRemoves = baseline.filter(
|
|
(c) => c === "remove_channel_member",
|
|
).length;
|
|
|
|
await openMembersSidebar(page, "general");
|
|
await openMemberMenu(page, agentPubkey);
|
|
await page.getByTestId(`sidebar-remove-member-${agentPubkey}`).click();
|
|
await expect(page.getByTestId(`sidebar-member-${agentPubkey}`)).toHaveCount(
|
|
0,
|
|
);
|
|
await page.keyboard.press("Escape");
|
|
await expect(page.getByTestId("members-sidebar")).not.toBeVisible();
|
|
|
|
await page.getByTestId("open-agents-view").click();
|
|
await expect(page.getByTestId(`managed-agent-${agentPubkey}`)).toHaveCount(1);
|
|
|
|
let commands = await readCommandLog(page);
|
|
// First removal: 1 remove_channel_member, agent record preserved.
|
|
expect(
|
|
commands.filter((c) => c === "remove_channel_member").length -
|
|
baselineRemoves,
|
|
).toBe(1);
|
|
|
|
await openMembersSidebar(page, secondChannelName);
|
|
await openMemberMenu(page, agentPubkey);
|
|
await page.getByTestId(`sidebar-remove-member-${agentPubkey}`).click();
|
|
await expect(page.getByTestId(`sidebar-member-${agentPubkey}`)).toHaveCount(
|
|
0,
|
|
);
|
|
await page.keyboard.press("Escape");
|
|
await expect(page.getByTestId("members-sidebar")).not.toBeVisible();
|
|
|
|
await page.getByTestId("open-agents-view").click();
|
|
await expect(page.getByTestId(`managed-agent-${agentPubkey}`)).toHaveCount(1);
|
|
|
|
commands = await readCommandLog(page);
|
|
// Second removal: agent is preserved even after removal from all channels.
|
|
expect(
|
|
commands.filter((c) => c === "remove_channel_member").length -
|
|
baselineRemoves,
|
|
).toBe(2);
|
|
});
|
|
|
|
test("bulk remove stays hidden when row-level remove is not allowed", async ({
|
|
page,
|
|
}) => {
|
|
const alicePubkey =
|
|
"953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f";
|
|
|
|
await page.goto("/");
|
|
|
|
// Join the "design" channel (unjoined by default) via the channel browser.
|
|
// The user becomes a regular member — not admin/owner.
|
|
await openChannelBrowser(page);
|
|
await expect(page.getByTestId("channel-browser-dialog")).toBeVisible();
|
|
await page
|
|
.getByTestId("browse-channel-design")
|
|
.getByRole("button", { name: "Join" })
|
|
.click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("design");
|
|
|
|
await openMembersSidebar(page, "design");
|
|
|
|
// Alice is a relay-observed bot in design (present in mockRelayAgents) that
|
|
// the user does not manage locally. Since there is no local managed agent
|
|
// for alice, hasActions is false and no 3-dot menu renders.
|
|
await expect(page.getByTestId(`sidebar-member-${alicePubkey}`)).toBeVisible();
|
|
await expect(
|
|
page.getByTestId(`sidebar-member-menu-${alicePubkey}`),
|
|
).toHaveCount(0);
|
|
|
|
// Bulk agent controls only render when hasControllableManagedBots is true.
|
|
// Since no bots in design have a local managed agent, the controls are absent.
|
|
await expect(page.getByTestId("members-sidebar-agent-controls")).toHaveCount(
|
|
0,
|
|
);
|
|
});
|
|
|
|
test("open channel management supports join and leave", async ({ page }) => {
|
|
await page.goto("/");
|
|
|
|
// Navigate to "design" (an unjoined channel) via the channel browser
|
|
await openChannelBrowser(page);
|
|
await expect(page.getByTestId("channel-browser-dialog")).toBeVisible();
|
|
await page
|
|
.getByTestId("browse-channel-design")
|
|
.getByRole("button", { name: "Join" })
|
|
.click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("design");
|
|
|
|
// Open members sidebar — should show current user after joining
|
|
await page.getByTestId("channel-members-trigger").click();
|
|
await expect(page.getByTestId("members-sidebar")).toBeVisible();
|
|
await expect(
|
|
page.getByTestId(`sidebar-member-${MOCK_IDENTITY_PUBKEY}`),
|
|
).toContainText("You");
|
|
await page.keyboard.press("Escape");
|
|
|
|
// Open channel management — should show Leave since we just joined
|
|
await page.getByTestId("channel-management-trigger").click();
|
|
await expect(page.getByTestId("channel-management-sheet")).toBeVisible();
|
|
await expect(page.getByTestId("channel-management-join")).toHaveCount(0);
|
|
await expect(page.getByTestId("channel-management-leave")).toBeVisible();
|
|
|
|
// Leave the channel
|
|
await page.getByTestId("channel-management-leave").click();
|
|
await expect(page.getByTestId("channel-management-sheet")).not.toBeVisible();
|
|
|
|
// After leaving, the app navigates away — re-open browser and find design
|
|
await openChannelBrowser(page);
|
|
await expect(page.getByTestId("channel-browser-dialog")).toBeVisible();
|
|
|
|
// "design" should be back in the unjoined section with a Join button
|
|
await expect(
|
|
page
|
|
.getByTestId("browse-channel-design")
|
|
.getByRole("button", { name: "Join" }),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test("manage channel can archive and unarchive a stream", async ({ page }) => {
|
|
await page.goto("/");
|
|
await openChannelManagement(page, "general");
|
|
|
|
await page.getByTestId("channel-management-archive").click();
|
|
await expect(page.getByTestId("channel-management-unarchive")).toBeVisible();
|
|
|
|
await closeChannelManagement(page);
|
|
await expect(page.getByTestId("stream-list")).not.toContainText("general");
|
|
await expect(page.getByTestId("message-input")).toHaveAttribute(
|
|
"contenteditable",
|
|
"false",
|
|
);
|
|
await expect(page.getByTestId("send-message")).toBeDisabled();
|
|
|
|
await openChannelBrowser(page);
|
|
await expect(page.getByTestId("channel-browser-dialog")).toBeVisible();
|
|
await expect(page.getByTestId("browse-channel-general")).toContainText(
|
|
"archived",
|
|
);
|
|
await page.getByTestId("browse-channel-general").click();
|
|
await expect(page.getByTestId("channel-browser-dialog")).not.toBeVisible();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
|
|
|
await page.getByTestId("channel-management-trigger").click();
|
|
await expect(page.getByTestId("channel-management-sheet")).toBeVisible();
|
|
await page.getByTestId("channel-management-unarchive").click();
|
|
await expect(page.getByTestId("channel-management-archive")).toBeVisible();
|
|
|
|
await closeChannelManagement(page);
|
|
await expect(page.getByTestId("stream-list")).toContainText("general");
|
|
await expect(page.getByTestId("message-input")).toHaveAttribute(
|
|
"contenteditable",
|
|
"true",
|
|
);
|
|
});
|
|
|
|
test("manage channel can delete an owned stream", async ({ page }) => {
|
|
const channelName = `delete-me-${Date.now()}`;
|
|
|
|
await page.goto("/");
|
|
await page.getByRole("button", { name: "Create a channel" }).click();
|
|
await page.getByTestId("create-channel-name").fill(channelName);
|
|
await page.getByTestId("create-channel-submit").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText(channelName);
|
|
|
|
await page.getByTestId("channel-management-trigger").click();
|
|
await expect(page.getByTestId("channel-management-sheet")).toBeVisible();
|
|
await page.getByTestId("channel-management-delete").click();
|
|
await expect(
|
|
page.getByTestId("channel-delete-confirmation-dialog"),
|
|
).toBeVisible();
|
|
await page.getByTestId("channel-delete-confirm").click();
|
|
|
|
await expect(page.getByTestId("home-inbox-list")).toBeVisible();
|
|
await expect(page.getByTestId("stream-list")).not.toContainText(channelName);
|
|
});
|
|
|
|
test("canceling channel deletion keeps the owned stream", async ({ page }) => {
|
|
const channelName = `keep-me-${Date.now()}`;
|
|
|
|
await page.goto("/");
|
|
await page.getByRole("button", { name: "Create a channel" }).click();
|
|
await page.getByTestId("create-channel-name").fill(channelName);
|
|
await page.getByTestId("create-channel-submit").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText(channelName);
|
|
|
|
await page.getByTestId("channel-management-trigger").click();
|
|
await expect(page.getByTestId("channel-management-sheet")).toBeVisible();
|
|
await page.getByTestId("channel-management-delete").click();
|
|
await expect(
|
|
page.getByTestId("channel-delete-confirmation-dialog"),
|
|
).toBeVisible();
|
|
await page.getByTestId("channel-delete-cancel").click();
|
|
|
|
await expect(
|
|
page.getByTestId("channel-delete-confirmation-dialog"),
|
|
).not.toBeVisible();
|
|
await expect(page.getByTestId("chat-title")).toHaveText(channelName);
|
|
await expect(page.getByTestId("stream-list")).toContainText(channelName);
|
|
});
|