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>
236 lines
7.9 KiB
TypeScript
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);
|
|
});
|
|
});
|