Files
buzz/desktop/tests/e2e/unread-pill.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

236 lines
7.9 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { TEST_IDENTITIES, installMockBridge } from "../helpers/bridge";
async function waitForMockLiveSubscription(
page: import("@playwright/test").Page,
channelName: string,
) {
await expect
.poll(async () => {
return page.evaluate(
({ ch }) =>
(
window as Window & {
__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?: (input: {
channelName: string;
}) => boolean;
}
).__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?.({ channelName: ch }) ??
false,
{ ch: channelName },
);
})
.toBe(true);
}
function emitMockMessage(
page: import("@playwright/test").Page,
channelName: string,
content: string,
createdAt?: number,
) {
return page.evaluate(
({ ch, msg, pubkey, ts }) => {
(
window as Window & {
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
channelName: string;
content: string;
pubkey: string;
createdAt?: number;
}) => unknown;
}
).__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName: ch,
content: msg,
pubkey,
createdAt: ts,
});
},
{
ch: channelName,
msg: content,
pubkey: TEST_IDENTITIES.alice.pubkey,
ts: createdAt,
},
);
}
// Unread messages must be created strictly after the read frontier captured
// when the channel was last open. The frontier is captured at the current
// second on open, and computeChannelUnreadMarker uses a strict
// `createdAt > frontier` predicate — so emitting at the same wall-clock second
// leaves the messages on the read side and the pill/divider never render.
// Dating them a minute ahead puts them deterministically past the frontier.
const UNREAD_OFFSET_SECONDS = 60;
function unreadTimestamp() {
return Math.floor(Date.now() / 1000) + UNREAD_OFFSET_SECONDS;
}
// Emit `count` unread messages to general, staggered one second apart so they
// sort deterministically and all land strictly past the read frontier.
async function emitUnreadMessages(
page: import("@playwright/test").Page,
count: number,
) {
const base = unreadTimestamp();
for (let index = 0; index < count; index += 1) {
await emitMockMessage(
page,
"general",
`Unread message ${index + 1}`,
base + index,
);
}
}
// Scroll the timeline up so the viewport is no longer pinned to the bottom.
// The pill auto-dismisses once the user reaches the bottom of the timeline, so
// it only stays rendered while scrolled up. Scrolling part-way (rather than to
// the very top) keeps real message context on screen instead of the channel's
// empty-state intro.
async function scrollTimelineUp(page: import("@playwright/test").Page) {
await page.getByTestId("message-timeline").evaluate((el) => {
el.scrollTop = Math.floor(el.scrollHeight * 0.35);
});
await page.waitForTimeout(300);
}
test.describe("unread pill & divider", () => {
test("01-unread-pill-visible", async ({ page }) => {
await installMockBridge(page);
await page.goto("/");
// Open general, then switch to random so general becomes inactive
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await waitForMockLiveSubscription(page, "general");
await page.getByTestId("channel-random").click();
await expect(page.getByTestId("chat-title")).toHaveText("random");
await emitUnreadMessages(page, 20);
// Switch back to general — pill should appear.
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
// Scroll up so the unreads sit below the fold: the pill is the
// "jump to oldest unread" affordance and only stays on screen while the
// user is scrolled away from the bottom of the timeline.
await scrollTimelineUp(page);
const pill = page.getByTestId("message-unread-pill");
await expect(pill).toBeVisible();
await expect(pill).toContainText("20 new messages");
await expect(
page.getByTestId("message-timeline-sticky-day-divider"),
).toHaveAttribute("data-day-label", /.+/);
const { pillTop, stickyDayTop } = await page.evaluate(() => {
const pill = document.querySelector<HTMLElement>(
'[data-testid="message-unread-pill"]',
);
const stickyDay = document.querySelector<HTMLElement>(
'[data-testid="message-timeline-sticky-day-divider"]',
);
if (!pill || !stickyDay) {
throw new Error("missing top timeline affordance");
}
return {
pillTop: pill.getBoundingClientRect().top,
stickyDayTop: stickyDay.getBoundingClientRect().top,
};
});
expect(Math.abs(pillTop - stickyDayTop)).toBeLessThanOrEqual(1);
});
test("02-unread-divider-visible", async ({ page }) => {
await installMockBridge(page);
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await waitForMockLiveSubscription(page, "general");
await page.getByTestId("channel-random").click();
await expect(page.getByTestId("chat-title")).toHaveText("random");
await emitUnreadMessages(page, 3);
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const divider = page.getByTestId("message-unread-divider");
await expect(divider).toBeVisible();
// Scroll the divider into view before asserting it.
await divider.scrollIntoViewIfNeeded();
await page.waitForTimeout(300);
});
test("03-pill-dismissed-after-scroll", async ({ page }) => {
await installMockBridge(page);
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await waitForMockLiveSubscription(page, "general");
await page.getByTestId("channel-random").click();
await expect(page.getByTestId("chat-title")).toHaveText("random");
await emitUnreadMessages(page, 20);
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
// Scroll up so the pill is showing, matching scenario 01's starting state.
await scrollTimelineUp(page);
const pill = page.getByTestId("message-unread-pill");
await expect(pill).toBeVisible();
// Click the pill to jump to the oldest unread, which dismisses it.
await pill.click();
// Pill should be dismissed
await expect(pill).toHaveCount(0);
const divider = page.getByTestId("message-unread-divider");
await expect(divider).toBeVisible();
});
test("04-mark-unread-suppresses-pill", async ({ page }) => {
await installMockBridge(page);
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
// Mark channel unread via context menu on the sidebar item
await page.getByTestId("channel-general").click({ button: "right" });
await page.getByText("Mark unread").click();
// Switch away and back to re-open the channel
await page.getByTestId("channel-random").click();
await expect(page.getByTestId("chat-title")).toHaveText("random");
// Forced channel unread uses channel-name emphasis, not the thread dot.
await expect(page.getByTestId("channel-general")).toHaveCSS(
"font-weight",
"700",
);
await expect(page.getByTestId("channel-unread-dot-general")).toHaveCount(0);
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
// Pill and divider should NOT appear (suppressed for forced-unread)
await expect(page.getByTestId("message-unread-pill")).toHaveCount(0);
await expect(page.getByTestId("message-unread-divider")).toHaveCount(0);
});
});