Files
buzz/desktop/tests/e2e/channel-activity-popover.spec.ts
T
cls 9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
feat: import Chinese-localized Buzz source snapshot
Signed-off-by: cls_宁波本机 <908705107@qq.com>
2026-08-13 18:34:25 +08:00

802 lines
26 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
import { waitForAnimations } from "../helpers/animations";
import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
const SELF_PUBKEY = "deadbeef".repeat(8);
const CHANNEL_GENERAL = "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50";
const AGENT_PUBKEY = TEST_IDENTITIES.charlie.pubkey;
type MockMessageEvent = {
id: string;
created_at: number;
pubkey: string;
};
type MockInboxFeedItem = {
content: string;
id: string;
tags: string[][];
};
async function waitForMockLiveSubscription(page: Page, channelName: string) {
await expect
.poll(() =>
page.evaluate(
(name) =>
(
window as Window & {
__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?: (input: {
channelName: string;
}) => boolean;
}
).__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?.({
channelName: name,
}) ?? false,
channelName,
),
)
.toBe(true);
}
async function emitMockMessage(
page: Page,
content: string,
options: {
parentEventId?: string;
pubkey: string;
createdAt: number;
mentionPubkeys?: string[];
},
): Promise<MockMessageEvent> {
const event = await page.evaluate(
({ body, parentEventId, pubkey, createdAt, mentionPubkeys }) =>
(
window as Window & {
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
channelName: string;
content: string;
parentEventId?: string;
pubkey: string;
createdAt: number;
mentionPubkeys?: string[];
}) => MockMessageEvent;
}
).__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName: "general",
content: body,
parentEventId,
pubkey,
createdAt,
mentionPubkeys,
}),
{
body: content,
parentEventId: options.parentEventId,
pubkey: options.pubkey,
createdAt: options.createdAt,
mentionPubkeys: options.mentionPubkeys,
},
);
if (!event) {
throw new Error("Mock message emitter is unavailable");
}
return event;
}
async function pushMockInboxFeedItems(page: Page, items: MockInboxFeedItem[]) {
await page.waitForFunction(
() =>
typeof (window as Window & { __BUZZ_E2E_PUSH_MOCK_FEED_ITEM__?: unknown })
.__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__ === "function",
);
await page.evaluate(
({ channelId, feedItems, senderPubkey }) => {
const pushFeedItem = (
window as Window & {
__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__?: (item: {
category: "mention";
channel_id: string;
channel_name: string;
channel_type: "stream";
content: string;
created_at: number;
id: string;
kind: number;
pubkey: string;
tags: string[][];
}) => void;
}
).__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__;
if (!pushFeedItem) {
throw new Error("Mock feed injection helper is unavailable");
}
const createdAt = Math.floor(Date.now() / 1_000) - 300;
for (const item of feedItems) {
pushFeedItem({
category: "mention",
channel_id: channelId,
channel_name: "general",
channel_type: "stream",
content: item.content,
created_at: createdAt,
id: item.id,
kind: 9,
pubkey: senderPubkey,
tags: item.tags,
});
}
},
{
channelId: CHANNEL_GENERAL,
feedItems: items,
senderPubkey: TEST_IDENTITIES.alice.pubkey,
},
);
}
async function getForcedUnreadSources(page: Page): Promise<string[]> {
return page.evaluate(
({ channelId, pubkey }) => {
const raw = window.localStorage.getItem(
`buzz-forced-unread.v1:${pubkey}`,
);
if (!raw) return [];
const entry = JSON.parse(raw)?.[channelId];
if (entry === undefined) return [];
return typeof entry === "object" && entry !== null
? (entry.sources ?? [])
: ["manual"];
},
{ channelId: CHANNEL_GENERAL, pubkey: SELF_PUBKEY },
);
}
async function seedChannelActivity(
page: Page,
{
extraThreadCount = 0,
includeAgent = true,
}: { extraThreadCount?: number; includeAgent?: boolean } = {},
) {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await waitForMockLiveSubscription(page, "general");
const ownRoot = await emitMockMessage(
page,
"How should the handoff work for longer agent tasks?",
{
pubkey: SELF_PUBKEY,
createdAt: Math.floor(Date.now() / 1000) - 20,
},
);
const extraRoots = await Promise.all(
Array.from({ length: extraThreadCount }, (_, index) =>
emitMockMessage(page, `Overflow preview thread ${index + 1}`, {
pubkey: SELF_PUBKEY,
createdAt: Math.floor(Date.now() / 1000) - 19 + index,
}),
),
);
await page.getByTestId("channel-random").click();
await expect(page.getByTestId("chat-title")).toHaveText("random");
const unreadAt = Math.floor(Date.now() / 1000) + 60;
await emitMockMessage(
page,
"I tightened the empty state and added the direct thread link.",
{
parentEventId: "mock-general-welcome",
pubkey: TEST_IDENTITIES.alice.pubkey,
createdAt: unreadAt,
mentionPubkeys: [SELF_PUBKEY],
},
);
await emitMockMessage(
page,
"The updated interaction keeps context visible without opening the channel first.",
{
parentEventId: ownRoot.id,
pubkey: TEST_IDENTITIES.bob.pubkey,
createdAt: unreadAt + 1,
},
);
await Promise.all(
extraRoots.map((root, index) =>
emitMockMessage(
page,
`A new reply in overflow preview thread ${index + 1}`,
{
parentEventId: root.id,
pubkey: TEST_IDENTITIES.alice.pubkey,
createdAt: unreadAt + 2 + index,
},
),
),
);
if (includeAgent) {
await page.waitForFunction(
() =>
typeof (window as Window & { __BUZZ_E2E_SEED_ACTIVE_TURNS__?: unknown })
.__BUZZ_E2E_SEED_ACTIVE_TURNS__ === "function",
);
await page.evaluate(
({ agentPubkey, channelId }) => {
(
window as Window & {
__BUZZ_E2E_SEED_ACTIVE_TURNS__?: (input: {
agentPubkey: string;
channelId: string;
turnId: string;
}) => void;
}
).__BUZZ_E2E_SEED_ACTIVE_TURNS__?.({
agentPubkey,
channelId,
turnId: "channel-hover-preview",
});
},
{ agentPubkey: AGENT_PUBKEY, channelId: CHANNEL_GENERAL },
);
}
await expect(page.getByTestId("channel-unread-dot-general")).toBeVisible();
if (includeAgent) {
await expect(page.getByTestId("channel-working-general")).toBeVisible();
}
}
async function openActivityPopover(page: Page) {
await page.getByTestId("channel-general").hover();
const popover = page.getByTestId("channel-activity-popover-general");
await expect(popover).toBeVisible();
return popover;
}
test.describe("channel activity hover preview", () => {
test.beforeEach(async ({ page }) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: AGENT_PUBKEY,
name: "Charlie",
status: "running",
channelNames: ["general"],
},
],
});
});
test("shows unread channel activity and working agents, then opens the selected thread", async ({
page,
}) => {
await seedChannelActivity(page, { extraThreadCount: 5 });
const popover = await openActivityPopover(page);
await waitForAnimations(page);
const enterScale = await popover.evaluate((element) =>
getComputedStyle(element).getPropertyValue("--tw-enter-scale"),
);
expect(enterScale).toBe("1");
const heading = popover.getByRole("heading", {
name: "Channel activity",
});
await expect(heading).toBeVisible();
await expect(heading).toHaveCSS("backdrop-filter", /blur/);
const activityScroll = popover.getByTestId("channel-activity-scroll");
await expect(activityScroll).toHaveCSS("overflow-y", "auto");
await expect(activityScroll.getByRole("heading")).toHaveCount(0);
await expect
.poll(() =>
activityScroll.evaluate(
(element) => element.scrollHeight > element.clientHeight,
),
)
.toBe(true);
await waitForAnimations(page);
const headingY = (await heading.boundingBox())?.y;
const scrollY = (await activityScroll.boundingBox())?.y;
const headingBottom = await heading.evaluate(
(element) => element.getBoundingClientRect().bottom,
);
expect(scrollY).toBeGreaterThanOrEqual(headingBottom - 1);
await activityScroll.evaluate((element) => {
element.scrollTop = element.scrollHeight;
});
await expect
.poll(() => activityScroll.evaluate((element) => element.scrollTop))
.toBeGreaterThan(0);
const scrolledHeadingY = (await heading.boundingBox())?.y;
expect(headingY).toBeDefined();
expect(scrolledHeadingY).toBeDefined();
expect(Math.abs((scrolledHeadingY ?? 0) - (headingY ?? 0))).toBeLessThan(
0.5,
);
await activityScroll.evaluate((element) => {
element.scrollTop = 0;
});
await expect
.poll(() =>
activityScroll.evaluate(
(element) =>
getComputedStyle(element, "::-webkit-scrollbar-thumb")
.backgroundColor,
),
)
.toMatch(/0\.15\)/);
await expect(popover.getByTestId(/^channel-activity-item-/)).toHaveCount(7);
await expect(popover).toContainText(
"I tightened the empty state and added the direct thread link.",
);
await expect(popover.getByRole("heading")).toHaveCount(1);
await expect(
popover.getByTestId(`channel-activity-agent-${AGENT_PUBKEY}`),
).toContainText("Charlie");
await expect(
popover.getByTestId(`channel-activity-agent-${AGENT_PUBKEY}`),
).toContainText("Working");
const orderedRows = popover.locator(
'[data-testid^="channel-activity-agent-"], [data-testid^="channel-activity-item-"]',
);
await expect(orderedRows.first()).toHaveAttribute(
"data-testid",
`channel-activity-agent-${AGENT_PUBKEY}`,
);
await waitForAnimations(page);
await page.screenshot({
clip: { x: 0, y: 80, width: 720, height: 640 },
path: "test-results/channel-activity-hover/channel-activity-popover.png",
});
const targetRow = popover
.getByTestId(/^channel-activity-item-/)
.filter({ hasText: "direct thread link" });
await targetRow.getByRole("button", { name: /Open thread/ }).click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const threadPanel = page.getByTestId("message-thread-panel");
await expect(threadPanel).toBeVisible();
await expect(threadPanel).toContainText("direct thread link");
});
test("removes the dot and preview after the final activity is read", async ({
page,
}) => {
await seedChannelActivity(page, { includeAgent: false });
const popover = await openActivityPopover(page);
await expect(popover.getByTestId(/^channel-activity-item-/)).toHaveCount(2);
for (let remaining = 1; remaining >= 0; remaining -= 1) {
const row = popover.getByTestId(/^channel-activity-item-/).first();
await row.hover();
await row.getByRole("button", { name: "Mark as read" }).click();
await expect(popover.getByTestId(/^channel-activity-item-/)).toHaveCount(
remaining,
);
}
await expect(page.getByTestId("channel-unread-dot-general")).toHaveCount(0);
await expect(
page.getByTestId("channel-activity-popover-general"),
).toHaveCount(0);
});
test("groups multiple unread replies against the previewed channel", async ({
page,
}) => {
await seedChannelActivity(page, { includeAgent: false });
await emitMockMessage(
page,
"A second unread reply should stay grouped with this thread.",
{
parentEventId: "mock-general-welcome",
pubkey: TEST_IDENTITIES.bob.pubkey,
createdAt: Math.floor(Date.now() / 1_000) + 120,
mentionPubkeys: [SELF_PUBKEY],
},
);
const popover = await openActivityPopover(page);
const groupedRow = popover
.getByTestId(/^channel-activity-item-/)
.filter({ hasText: "direct thread link" });
await expect(groupedRow).toContainText("2 unread");
});
test("marks projected thread activity read from the active channel menu", async ({
page,
}) => {
await seedChannelActivity(page, { includeAgent: false });
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await expect(page.getByTestId("channel-unread-dot-general")).toBeVisible();
await page.getByTestId("channel-general").click({ button: "right" });
await expect(
page.getByRole("menuitem", { name: "Mark as read" }),
).toBeVisible();
await expect(
page.getByRole("menuitem", { name: "Mark unread" }),
).toHaveCount(0);
await page.getByRole("menuitem", { name: "Mark as read" }).click();
await expect(page.getByTestId("channel-unread-dot-general")).toHaveCount(0);
});
test("marks projected thread activity read in the active channel with Shift+Escape", async ({
page,
}) => {
await seedChannelActivity(page, { includeAgent: false });
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await expect(page.getByTestId("channel-unread-dot-general")).toBeVisible();
const shortcutHandled = await page.evaluate(() => {
const event = new KeyboardEvent("keydown", {
bubbles: true,
cancelable: true,
key: "Escape",
shiftKey: true,
});
window.dispatchEvent(event);
return event.defaultPrevented;
});
expect(shortcutHandled).toBe(true);
await expect(page.getByTestId("channel-unread-dot-general")).toHaveCount(0);
await page.getByTestId("channel-general").hover();
await expect(
page.getByTestId("channel-activity-popover-general"),
).toHaveCount(0);
});
test("supports row actions and opens an agent's scoped activity", async ({
page,
}) => {
await seedChannelActivity(page);
let popover = await openActivityPopover(page);
const initialRows = popover.getByTestId(/^channel-activity-item-/);
await expect(initialRows).toHaveCount(2);
const firstRow = initialRows.first();
await firstRow.hover();
await firstRow.getByRole("button", { name: "Mark as read" }).click();
await expect(popover.getByTestId(/^channel-activity-item-/)).toHaveCount(1);
const remainingRow = popover.getByTestId(/^channel-activity-item-/).first();
await remainingRow.hover();
await remainingRow.getByRole("button", { name: "Remind me later" }).click();
await expect(
page.getByRole("dialog").getByText("Remind me later"),
).toBeVisible();
await page.getByRole("button", { name: "Cancel" }).click();
popover = await openActivityPopover(page);
await popover.getByTestId(`channel-activity-agent-${AGENT_PUBKEY}`).click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await expect(page.getByTestId("agent-session-agent-name")).toContainText(
"Charlie",
);
await expect(page.getByTestId("agent-session-scope-label")).toContainText(
"#general",
);
});
test("keeps multiple Inbox threads visible after opening their channel", async ({
page,
}) => {
const inboxItemIds = [
"older-inbox-thread-for-hover",
"second-inbox-thread-for-hover",
];
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await page.getByRole("button", { name: "Inbox", exact: true }).click();
await expect(page.getByTestId("home-inbox-list")).toBeVisible();
await pushMockInboxFeedItems(
page,
inboxItemIds.map((id, index) => ({
content:
index === 0
? "Older Inbox thread reopened for hover testing."
: "A second Inbox thread reopened for hover testing.",
id,
tags: [
["h", CHANNEL_GENERAL],
[
"e",
index === 0
? "older-inbox-thread-root"
: "second-inbox-thread-root",
"",
"root",
],
[
"e",
index === 0
? "older-inbox-thread-parent"
: "second-inbox-thread-parent",
"",
"reply",
],
["p", TEST_IDENTITIES.tyler.pubkey],
],
})),
);
for (const itemId of inboxItemIds) {
const inboxRow = page.getByTestId(`home-inbox-item-${itemId}`);
await expect(inboxRow).toBeVisible();
await inboxRow.hover();
await inboxRow.getByRole("button", { name: "Mark as read" }).click();
await inboxRow.hover();
await inboxRow.getByRole("button", { name: "Mark unread" }).click();
}
await expect(page.getByTestId("channel-general")).toHaveCSS(
"font-weight",
"700",
);
for (const [index, itemId] of inboxItemIds.entries()) {
const inboxRow = page.getByTestId(`home-inbox-item-${itemId}`);
await inboxRow.hover();
await inboxRow.getByRole("button", { name: "Mark as read" }).click();
if (index === 0) {
await expect(page.getByTestId("channel-general")).toHaveCSS(
"font-weight",
"700",
);
}
}
await expect(page.getByTestId("channel-general")).not.toHaveCSS(
"font-weight",
"700",
);
for (const itemId of inboxItemIds) {
const inboxRow = page.getByTestId(`home-inbox-item-${itemId}`);
await inboxRow.hover();
await inboxRow.getByRole("button", { name: "Mark unread" }).click();
}
await expect(page.getByTestId("channel-general")).toHaveCSS(
"font-weight",
"700",
);
await expect(page.getByTestId("channel-unread-dot-general")).toBeVisible();
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await expect(page.getByTestId("channel-unread-dot-general")).toBeVisible();
await page.mouse.move(900, 680);
let popover = await openActivityPopover(page);
await expect(popover.getByTestId(/^channel-activity-item-/)).toHaveCount(2);
await expect(popover).toContainText("Older Inbox thread reopened");
await expect(popover).toContainText("A second Inbox thread reopened");
await expect(popover.getByText("Thread", { exact: true })).toHaveCount(2);
await popover
.getByTestId(/^channel-activity-item-/)
.filter({ hasText: "Older Inbox thread reopened" })
.getByRole("button", { name: /Open thread/ })
.click();
await expect(popover).toBeHidden();
await page.mouse.move(900, 680);
popover = await openActivityPopover(page);
await expect(popover.getByTestId(/^channel-activity-item-/)).toHaveCount(1);
await expect(popover).not.toContainText("Older Inbox thread reopened");
await expect(page.getByTestId("channel-unread-dot-general")).toBeVisible();
await expect(page.getByTestId("channel-general")).toHaveCSS(
"font-weight",
"400",
);
await page.mouse.move(900, 680);
await expect(popover).toBeHidden();
await page.getByRole("button", { name: "Inbox", exact: true }).click();
const topLevelItemId = "top-level-inbox-item-for-channel-read";
await pushMockInboxFeedItems(page, [
{
content: "Top-level Inbox item reopened for channel read testing.",
id: topLevelItemId,
tags: [
["h", CHANNEL_GENERAL],
["p", TEST_IDENTITIES.tyler.pubkey],
],
},
]);
const topLevelInboxRow = page.getByTestId(
`home-inbox-item-${topLevelItemId}`,
);
await expect(topLevelInboxRow).toBeVisible();
await topLevelInboxRow.hover();
await topLevelInboxRow.getByRole("button", { name: "Mark unread" }).click();
await page.getByTestId("channel-general").click({ button: "right" });
await page.getByRole("menuitem", { name: "Mark as read" }).click();
await topLevelInboxRow.hover();
await expect(
topLevelInboxRow.getByRole("button", { name: "Mark unread" }),
).toBeVisible();
await expect(page.getByTestId("channel-unread-dot-general")).toHaveCount(0);
await page.getByTestId("channel-general").hover();
await expect(
page.getByTestId("channel-activity-popover-general"),
).toHaveCount(0);
await page.getByTestId("channel-random").click();
await expect(page.getByTestId("chat-title")).toHaveText("random");
await expect(page.getByTestId("channel-general")).not.toHaveCSS(
"font-weight",
"700",
);
});
test("reading a grouped Inbox thread preserves an unrelated manual unread", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await waitForMockLiveSubscription(page, "general");
const manualUnreadMessage = await emitMockMessage(
page,
"Keep this separate timeline message unread.",
{
pubkey: TEST_IDENTITIES.bob.pubkey,
createdAt: Math.floor(Date.now() / 1_000),
},
);
const manualUnreadRow = page
.getByTestId("message-row")
.filter({ hasText: "Keep this separate timeline message unread." });
await manualUnreadRow.hover();
await page.getByTestId(`more-actions-${manualUnreadMessage.id}`).click();
await page
.getByTestId(`mark-read-toggle-${manualUnreadMessage.id}`)
.click();
await page.getByRole("button", { name: "Inbox", exact: true }).click();
const groupedRootId = "grouped-inbox-root-preserve-manual";
const groupedReplyId = "grouped-inbox-reply-preserve-manual";
await pushMockInboxFeedItems(page, [
{
content: "Grouped Inbox root for manual unread preservation.",
id: groupedRootId,
tags: [
["h", CHANNEL_GENERAL],
["p", TEST_IDENTITIES.tyler.pubkey],
],
},
{
content: "Grouped Inbox reply marked read independently.",
id: groupedReplyId,
tags: [
["h", CHANNEL_GENERAL],
["e", groupedRootId, "", "root"],
["e", groupedRootId, "", "reply"],
["p", TEST_IDENTITIES.tyler.pubkey],
],
},
]);
const groupedInboxRow = page.getByTestId(
`home-inbox-item-${groupedReplyId}`,
);
await expect(groupedInboxRow).toBeVisible();
await groupedInboxRow.hover();
await groupedInboxRow.getByRole("button", { name: "Mark as read" }).click();
await page.getByTestId("channel-random").click();
await expect(page.getByTestId("channel-general")).toHaveCSS(
"font-weight",
"700",
);
});
test("preserves Inbox ownership while another top-level row remains unread", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await page.getByRole("button", { name: "Inbox", exact: true }).click();
const topLevelItemIds = [
"first-top-level-inbox-owner",
"second-top-level-inbox-owner",
];
await pushMockInboxFeedItems(
page,
topLevelItemIds.map((id, index) => ({
content: `Top-level Inbox owner ${index + 1}.`,
id,
tags: [
["h", CHANNEL_GENERAL],
["p", TEST_IDENTITIES.tyler.pubkey],
],
})),
);
for (const itemId of topLevelItemIds) {
const inboxRow = page.getByTestId(`home-inbox-item-${itemId}`);
await expect(inboxRow).toBeVisible();
await inboxRow.hover();
await inboxRow.getByRole("button", { name: "Mark unread" }).click();
}
await expect(page.getByTestId("channel-general")).toHaveCSS(
"font-weight",
"700",
);
await expect.poll(() => getForcedUnreadSources(page)).toEqual(["inbox"]);
await page.getByRole("button", { name: "Inbox", exact: true }).click();
for (const [index, itemId] of topLevelItemIds.entries()) {
const inboxRow = page.getByTestId(`home-inbox-item-${itemId}`);
await inboxRow.hover();
await inboxRow.getByRole("button", { name: "Mark as read" }).click();
if (index === 0) {
await expect(page.getByTestId("channel-general")).toHaveCSS(
"font-weight",
"700",
);
await expect
.poll(() => getForcedUnreadSources(page))
.toEqual(["inbox"]);
}
}
await expect.poll(() => getForcedUnreadSources(page)).toEqual([]);
});
test("surfaces future replies after the user reacts to a thread root", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await waitForMockLiveSubscription(page, "general");
const root = await emitMockMessage(
page,
"Reacting here means I care about future replies.",
{
pubkey: TEST_IDENTITIES.alice.pubkey,
createdAt: Math.floor(Date.now() / 1_000) - 20,
},
);
const rootRow = page
.getByTestId("message-row")
.filter({ hasText: "Reacting here means I care" });
await expect(rootRow).toBeVisible();
await rootRow.hover();
const actionBar = page.getByTestId(`message-action-bar-${root.id}`);
await expect(actionBar).toBeVisible();
await actionBar
.getByRole("button", { name: /^React with / })
.first()
.click();
await expect(
rootRow.getByRole("button", { name: /^Toggle .* reaction$/ }),
).toBeVisible();
await page.getByTestId("channel-random").click();
await emitMockMessage(
page,
"This follow-up appears because the root was reacted to.",
{
parentEventId: root.id,
pubkey: TEST_IDENTITIES.bob.pubkey,
createdAt: Math.floor(Date.now() / 1_000) + 60,
},
);
await expect(page.getByTestId("channel-unread-dot-general")).toBeVisible();
const popover = await openActivityPopover(page);
await expect(popover).toContainText(
"This follow-up appears because the root was reacted to.",
);
});
});