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,235 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user