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
Signed-off-by: cls_宁波本机 <908705107@qq.com>
360 lines
12 KiB
TypeScript
360 lines
12 KiB
TypeScript
/**
|
|
* Screenshots documenting the focused Inbox behavior for PR #2045.
|
|
*
|
|
* These replace the hand-generated shots posted earlier in that PR, which went
|
|
* stale twice: once when the surface was briefly renamed to "Activity", and
|
|
* again when the Projects filter landed. Keeping them in a spec means the next
|
|
* round regenerates instead of drifting.
|
|
*
|
|
* Run: pnpm build:e2e && pnpm exec playwright test --project=smoke \
|
|
* tests/e2e/inbox-refactor-screenshots.spec.ts
|
|
* Output: test-results/inbox-refactor/
|
|
*/
|
|
import { expect, test } from "@playwright/test";
|
|
|
|
import { waitForAnimations } from "../helpers/animations";
|
|
import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
|
|
|
|
const SHOTS = "test-results/inbox-refactor";
|
|
|
|
const GENERAL_CHANNEL_ID = "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50";
|
|
const AGENTS_CHANNEL_ID = "94a444a4-c0a3-5966-ab05-530c6ddc2301";
|
|
const DM_CHANNEL_ID = "f48efb06-0c93-5025-aac9-2e646bb6bfa8";
|
|
|
|
// Mock bridge default pubkey — must match DEFAULT_MOCK_PUBKEY in bridge.ts.
|
|
const MOCK_PUBKEY = "deadbeef".repeat(8);
|
|
const DRAFT_STORE_KEY = `buzz-drafts.v1:${MOCK_PUBKEY}`;
|
|
|
|
// Fixed timestamps so draft ordering renders deterministically.
|
|
const DRAFT_CREATED_AT_1 = "2026-07-01T10:00:00.000Z";
|
|
const DRAFT_CREATED_AT_2 = "2026-07-02T14:30:00.000Z";
|
|
|
|
type MockFeedWindow = Window & {
|
|
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
|
|
channelName: string;
|
|
content: string;
|
|
createdAt?: number;
|
|
id?: string;
|
|
parentEventId?: string;
|
|
pubkey?: string;
|
|
}) => {
|
|
content: string;
|
|
created_at: number;
|
|
id: string;
|
|
kind: number;
|
|
pubkey: string;
|
|
tags: string[][];
|
|
};
|
|
__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__?: (item: {
|
|
category: "mention" | "needs_action" | "activity" | "agent_activity";
|
|
channel_id: string | null;
|
|
channel_name: string;
|
|
channel_type?: string | null;
|
|
content: string;
|
|
created_at: number;
|
|
id: string;
|
|
kind: number;
|
|
pubkey: string;
|
|
tags: string[][];
|
|
}) => void;
|
|
};
|
|
|
|
/**
|
|
* The mock community is seeded without a pubkey, which initDraftStore needs on
|
|
* startup. Init scripts run in registration order, so this patches the seed
|
|
* installMockBridge already wrote.
|
|
*/
|
|
async function patchCommunityPubkey(page: import("@playwright/test").Page) {
|
|
await page.addInitScript(
|
|
({ pubkey }) => {
|
|
const raw = window.localStorage.getItem("buzz-communities");
|
|
const communities = raw
|
|
? (JSON.parse(raw) as Array<Record<string, unknown>>)
|
|
: [];
|
|
if (communities[0]) {
|
|
communities[0].pubkey = pubkey;
|
|
window.localStorage.setItem(
|
|
"buzz-communities",
|
|
JSON.stringify(communities),
|
|
);
|
|
}
|
|
},
|
|
{ pubkey: MOCK_PUBKEY },
|
|
);
|
|
}
|
|
|
|
/** Two active drafts so the Drafts filter renders its count badge. */
|
|
async function seedTwoDrafts(page: import("@playwright/test").Page) {
|
|
await page.addInitScript(
|
|
({ storeKey, channelId, agentsChannelId, createdAt1, createdAt2 }) => {
|
|
const draft = (content: string, channel: string, timestamp: string) => ({
|
|
content,
|
|
selectionStart: content.length,
|
|
selectionEnd: content.length,
|
|
channelId: channel,
|
|
createdAt: timestamp,
|
|
updatedAt: timestamp,
|
|
pendingImeta: [],
|
|
spoileredAttachmentUrls: [],
|
|
status: "active" as const,
|
|
});
|
|
|
|
window.localStorage.setItem(
|
|
storeKey,
|
|
JSON.stringify({
|
|
[`channel:${channelId}`]: draft(
|
|
"Drafting the release notes — will post once the checklist is signed off.",
|
|
channelId,
|
|
createdAt1,
|
|
),
|
|
[`channel:${agentsChannelId}`]: draft(
|
|
"Following up on the harness rollout.",
|
|
agentsChannelId,
|
|
createdAt2,
|
|
),
|
|
}),
|
|
);
|
|
},
|
|
{
|
|
storeKey: DRAFT_STORE_KEY,
|
|
channelId: GENERAL_CHANNEL_ID,
|
|
agentsChannelId: AGENTS_CHANNEL_ID,
|
|
createdAt1: DRAFT_CREATED_AT_1,
|
|
createdAt2: DRAFT_CREATED_AT_2,
|
|
},
|
|
);
|
|
}
|
|
|
|
/** Wait for the live mock-feed helpers the seeding below depends on. */
|
|
async function waitForMockFeedHelpers(page: import("@playwright/test").Page) {
|
|
await expect(page.getByTestId("home-inbox-list")).toBeVisible({
|
|
timeout: 10_000,
|
|
});
|
|
await page.waitForFunction(() => {
|
|
const win = window as MockFeedWindow;
|
|
return (
|
|
typeof win.__BUZZ_E2E_EMIT_MOCK_MESSAGE__ === "function" &&
|
|
typeof win.__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__ === "function"
|
|
);
|
|
});
|
|
}
|
|
|
|
test.describe("inbox refactor screenshots", () => {
|
|
test.use({ viewport: { width: 1280, height: 900 } });
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
page.on("pageerror", (err) => console.error("PAGE ERROR:", err.message));
|
|
page.on("console", (msg) => {
|
|
if (msg.type() === "error") {
|
|
console.error("CONSOLE ERROR:", msg.text().slice(0, 300));
|
|
}
|
|
});
|
|
});
|
|
|
|
test("01 — filter menu with Reminders/Drafts divider and counts", async ({
|
|
page,
|
|
}) => {
|
|
await patchCommunityPubkey(page);
|
|
await seedTwoDrafts(page);
|
|
await installMockBridge(page, { mode: "mock" });
|
|
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await expect(page.getByTestId("home-inbox")).toBeVisible({
|
|
timeout: 10_000,
|
|
});
|
|
|
|
await page.getByTestId("inbox-filter-trigger").click();
|
|
const menu = page.getByRole("menu");
|
|
await expect(menu).toBeVisible();
|
|
await expect(
|
|
menu.getByRole("menuitemradio", { name: "Projects" }),
|
|
).toBeVisible();
|
|
await waitForAnimations(page);
|
|
|
|
await page.screenshot({ path: `${SHOTS}/01-current-filters.png` });
|
|
});
|
|
|
|
test("02 — Inbox label, inbox icon, and overflow controls", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(page, { mode: "mock" });
|
|
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await expect(page.getByTestId("home-inbox")).toBeVisible({
|
|
timeout: 10_000,
|
|
});
|
|
|
|
// The sidebar must be in frame — the label is the point of this shot.
|
|
const inboxButton = page
|
|
.getByTestId("sidebar-primary-menu")
|
|
.getByRole("button", { name: "Inbox", exact: true });
|
|
await expect(inboxButton).toBeVisible();
|
|
// Inbox is a destination, not a notification tray, so it carries the inbox
|
|
// glyph rather than a bell. Asserted because nothing else pins the icon.
|
|
await expect(inboxButton.locator("svg.lucide-inbox")).toHaveCount(1);
|
|
|
|
await page.getByTestId("inbox-options-trigger").click();
|
|
await expect(page.getByText("Show unread only")).toBeVisible();
|
|
await expect(page.getByText("Mark all as read")).toBeVisible();
|
|
await waitForAnimations(page);
|
|
|
|
await page.screenshot({ path: `${SHOTS}/02-current-controls.png` });
|
|
});
|
|
|
|
test("03 — consecutive DMs group into one conversation row", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(page, { mode: "mock" });
|
|
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await waitForMockFeedHelpers(page);
|
|
|
|
const dmIds = ["shot-dm-first", "shot-dm-second", "shot-dm-third"];
|
|
await page.evaluate(
|
|
({ channelId, createdAt, ids, senderPubkey }) => {
|
|
const win = window as MockFeedWindow;
|
|
const emitMessage = win.__BUZZ_E2E_EMIT_MOCK_MESSAGE__;
|
|
const pushFeedItem = win.__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__;
|
|
if (!emitMessage || !pushFeedItem) {
|
|
throw new Error("Mock bridge helpers are not installed.");
|
|
}
|
|
|
|
[
|
|
"Quick update: the staging deploy is ready.",
|
|
"I ran the smoke tests and everything passed.",
|
|
"Can you take a final look before we release?",
|
|
].forEach((content, index) => {
|
|
const event = emitMessage({
|
|
channelName: "alice-tyler",
|
|
content,
|
|
createdAt: createdAt + index,
|
|
id: ids[index],
|
|
pubkey: senderPubkey,
|
|
});
|
|
pushFeedItem({
|
|
category: "activity",
|
|
channel_id: channelId,
|
|
channel_name: "alice-tyler",
|
|
channel_type: null,
|
|
content: event.content,
|
|
created_at: event.created_at,
|
|
id: event.id,
|
|
kind: event.kind,
|
|
pubkey: event.pubkey,
|
|
tags: event.tags,
|
|
});
|
|
});
|
|
},
|
|
{
|
|
channelId: DM_CHANNEL_ID,
|
|
createdAt: Math.floor(Date.now() / 1000),
|
|
ids: dmIds,
|
|
senderPubkey: TEST_IDENTITIES.alice.pubkey,
|
|
},
|
|
);
|
|
|
|
const firstDmRow = page.getByTestId(`home-inbox-item-${dmIds[0]}`);
|
|
await expect(firstDmRow).toBeVisible();
|
|
// The grouping claim: three DMs, one row.
|
|
await expect(page.getByTestId(`home-inbox-item-${dmIds[1]}`)).toHaveCount(
|
|
0,
|
|
);
|
|
await expect(page.getByTestId(`home-inbox-item-${dmIds[2]}`)).toHaveCount(
|
|
0,
|
|
);
|
|
|
|
await firstDmRow.click();
|
|
const detail = page.getByTestId("home-inbox-detail");
|
|
await expect(detail.getByRole("heading")).toHaveText("DM with alice");
|
|
await expect(detail.getByTestId("message-unread-divider")).toBeVisible();
|
|
await waitForAnimations(page);
|
|
|
|
await page.screenshot({ path: `${SHOTS}/03-grouped-dms.png` });
|
|
});
|
|
|
|
test("04 — thread opens at the oldest unread reply", async ({ page }) => {
|
|
await installMockBridge(page, { mode: "mock" });
|
|
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await waitForMockFeedHelpers(page);
|
|
|
|
const replyIds = [
|
|
"shot-reply-first",
|
|
"shot-reply-second",
|
|
"shot-reply-third",
|
|
];
|
|
await page.evaluate(
|
|
({ agentPubkeys, channelId, currentPubkey, ids }) => {
|
|
const win = window as MockFeedWindow;
|
|
const emitMessage = win.__BUZZ_E2E_EMIT_MOCK_MESSAGE__;
|
|
const pushFeedItem = win.__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__;
|
|
if (!emitMessage || !pushFeedItem) {
|
|
throw new Error("Mock bridge helpers are not installed.");
|
|
}
|
|
|
|
const createdAt = Math.floor(Date.now() / 1000) + 60;
|
|
const root = emitMessage({
|
|
channelName: "general",
|
|
content: "Release checklist for the desktop build",
|
|
createdAt: createdAt - 10,
|
|
id: "shot-thread-root",
|
|
pubkey: currentPubkey,
|
|
});
|
|
|
|
[
|
|
"Started on the changelog — first pass is up.",
|
|
"Verified the signed build on macOS.",
|
|
"One open question on the version suffix.",
|
|
].forEach((content, index) => {
|
|
const event = emitMessage({
|
|
channelName: "general",
|
|
content,
|
|
createdAt: createdAt + index,
|
|
id: ids[index],
|
|
parentEventId: root.id,
|
|
pubkey: agentPubkeys[index % agentPubkeys.length],
|
|
});
|
|
pushFeedItem({
|
|
category: "activity",
|
|
channel_id: channelId,
|
|
channel_name: "general",
|
|
channel_type: "stream",
|
|
content: event.content,
|
|
created_at: event.created_at,
|
|
id: event.id,
|
|
kind: event.kind,
|
|
pubkey: event.pubkey,
|
|
tags: event.tags,
|
|
});
|
|
});
|
|
},
|
|
{
|
|
agentPubkeys: [
|
|
TEST_IDENTITIES.alice.pubkey,
|
|
TEST_IDENTITIES.charlie.pubkey,
|
|
],
|
|
channelId: GENERAL_CHANNEL_ID,
|
|
currentPubkey: MOCK_PUBKEY,
|
|
ids: replyIds,
|
|
},
|
|
);
|
|
|
|
const firstUnreadRow = page.getByTestId(`home-inbox-item-${replyIds[0]}`);
|
|
await expect(firstUnreadRow).toBeVisible();
|
|
await firstUnreadRow.click();
|
|
|
|
const detail = page.getByTestId("home-inbox-detail");
|
|
// The anchoring claim: full thread present, selection on the oldest unread.
|
|
await expect(detail).toContainText(
|
|
"Release checklist for the desktop build",
|
|
);
|
|
await expect(detail.getByTestId("message-unread-divider")).toBeVisible();
|
|
await expect(page.getByTestId("home-inbox-selected-message")).toContainText(
|
|
"Started on the changelog — first pass is up.",
|
|
);
|
|
await waitForAnimations(page);
|
|
|
|
await page.screenshot({ path: `${SHOTS}/04-thread-context.png` });
|
|
});
|
|
});
|