feat: import Chinese-localized Buzz source snapshot
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
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
Signed-off-by: cls_宁波本机 <908705107@qq.com>
This commit is contained in:
@@ -0,0 +1,801 @@
|
||||
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.",
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user