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,534 @@
|
||||
import { expect, test, type Browser, type Page } from "@playwright/test";
|
||||
|
||||
import {
|
||||
installRelayBridge,
|
||||
openChannelBrowser,
|
||||
openCreateChannelDialog,
|
||||
TEST_IDENTITIES,
|
||||
} from "../helpers/bridge";
|
||||
import { assertRelaySeeded } from "../helpers/seed";
|
||||
|
||||
const isCi = Boolean(process.env.CI);
|
||||
const relaySeedHookTimeoutMs = isCi ? 90_000 : 30_000;
|
||||
|
||||
async function expectTimelineToContain(page: Page, text: string) {
|
||||
await expect(page.getByTestId("message-timeline")).toContainText(text);
|
||||
}
|
||||
|
||||
async function getTimelineMetrics(page: Page) {
|
||||
return page.getByTestId("message-timeline").evaluate((element) => {
|
||||
const timeline = element as HTMLDivElement;
|
||||
|
||||
const composer = document.querySelector<HTMLElement>(
|
||||
'[data-testid="channel-composer-overlay"]',
|
||||
);
|
||||
const composerHeight = composer?.getBoundingClientRect().height ?? 0;
|
||||
|
||||
return {
|
||||
clientHeight: timeline.clientHeight,
|
||||
scrollHeight: timeline.scrollHeight,
|
||||
scrollTop: timeline.scrollTop,
|
||||
composerHeight,
|
||||
// The virtualized timeline reserves a trailing spacer equal to the
|
||||
// overlaid composer. Reaching the visual tail therefore leaves that
|
||||
// spacer below the last row rather than setting the raw DOM distance to 0.
|
||||
distanceFromBottom:
|
||||
timeline.scrollHeight -
|
||||
timeline.clientHeight -
|
||||
timeline.scrollTop -
|
||||
composerHeight,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function seedTimelineHistory(
|
||||
senderPage: Page,
|
||||
channelName: string,
|
||||
prefix: string,
|
||||
) {
|
||||
const messages = Array.from(
|
||||
{ length: 24 },
|
||||
(_, index) => `${prefix} seed ${index}`,
|
||||
);
|
||||
for (const content of messages) {
|
||||
await sendChannelMessage(senderPage, { channelName, content });
|
||||
}
|
||||
const lastSeed = messages.at(-1);
|
||||
if (!lastSeed) throw new Error("Timeline seed set is empty.");
|
||||
return lastSeed;
|
||||
}
|
||||
|
||||
async function createAndJoinSharedStream(
|
||||
ownerPage: Page,
|
||||
memberPage: Page,
|
||||
channelName: string,
|
||||
seed?: { prefix: string; minDistance?: number },
|
||||
) {
|
||||
await openCreateChannelDialog(ownerPage);
|
||||
await ownerPage.getByTestId("create-channel-name").fill(channelName);
|
||||
await ownerPage.getByTestId("create-channel-submit").click();
|
||||
await expect(ownerPage.getByTestId("stream-list")).toContainText(channelName);
|
||||
await expect(ownerPage.getByTestId("chat-title")).toHaveText(channelName);
|
||||
await expect(ownerPage.getByTestId("message-input")).toBeEnabled();
|
||||
|
||||
const lastSeed = seed
|
||||
? await seedTimelineHistory(ownerPage, channelName, seed.prefix)
|
||||
: null;
|
||||
|
||||
await openChannelBrowser(memberPage);
|
||||
await expect(memberPage.getByTestId("channel-browser-dialog")).toBeVisible();
|
||||
await memberPage
|
||||
.getByTestId(`browse-channel-${channelName}`)
|
||||
.getByRole("button", { name: "Join" })
|
||||
.click();
|
||||
await expect(memberPage.getByTestId("chat-title")).toHaveText(channelName);
|
||||
await expect(memberPage.getByTestId("stream-list")).toContainText(
|
||||
channelName,
|
||||
);
|
||||
|
||||
await expect(memberPage.getByTestId("message-input")).toBeEnabled();
|
||||
|
||||
if (lastSeed) {
|
||||
await expectTimelineToContain(memberPage, lastSeed);
|
||||
const metrics = await getTimelineMetrics(memberPage);
|
||||
expect(metrics.scrollHeight).toBeGreaterThan(
|
||||
metrics.clientHeight +
|
||||
metrics.composerHeight +
|
||||
(seed?.minDistance ?? 160),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendChannelMessage(
|
||||
page: Page,
|
||||
{
|
||||
channelName,
|
||||
content,
|
||||
mentionPubkeys,
|
||||
}: {
|
||||
channelName: string;
|
||||
content: string;
|
||||
mentionPubkeys?: string[];
|
||||
},
|
||||
) {
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const tauriWindow = window as Window & {
|
||||
__TAURI_INTERNALS__?: { invoke?: unknown };
|
||||
};
|
||||
|
||||
return typeof tauriWindow.__TAURI_INTERNALS__?.invoke === "function";
|
||||
},
|
||||
null,
|
||||
{ timeout: 5_000 },
|
||||
);
|
||||
|
||||
await page.evaluate(
|
||||
async ({ channelName: targetChannelName, content, mentionPubkeys }) => {
|
||||
const tauriWindow = window as Window & {
|
||||
__TAURI_INTERNALS__?: {
|
||||
invoke: (
|
||||
command: string,
|
||||
payload?: Record<string, unknown>,
|
||||
) => Promise<unknown>;
|
||||
};
|
||||
};
|
||||
|
||||
const invoke = tauriWindow.__TAURI_INTERNALS__?.invoke;
|
||||
if (!invoke) {
|
||||
throw new Error("Tauri invoke bridge is unavailable.");
|
||||
}
|
||||
|
||||
const channels = (await invoke("get_channels")) as Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
}>;
|
||||
const channel = channels.find(({ name }) => name === targetChannelName);
|
||||
if (!channel) {
|
||||
throw new Error(`Channel not found: ${targetChannelName}`);
|
||||
}
|
||||
|
||||
await invoke("send_channel_message", {
|
||||
channelId: channel.id,
|
||||
content,
|
||||
kind: null,
|
||||
mediaTags: null,
|
||||
mentionPubkeys: mentionPubkeys ?? null,
|
||||
parentEventId: null,
|
||||
});
|
||||
},
|
||||
{ channelName, content, mentionPubkeys },
|
||||
);
|
||||
}
|
||||
|
||||
async function scrollTimelineAwayFromBottom(page: Page, minDistance = 160) {
|
||||
const timeline = page.getByTestId("message-timeline");
|
||||
await timeline.hover();
|
||||
|
||||
for (let attempt = 0; attempt < 8; attempt += 1) {
|
||||
await page.mouse.wheel(0, -800);
|
||||
const metrics = await getTimelineMetrics(page);
|
||||
if (metrics.distanceFromBottom > minDistance) {
|
||||
// The DOM scroll position is now off the bottom, but the timeline's
|
||||
// `onScroll` handler updates the React "away from bottom" anchor state
|
||||
// asynchronously. If a new message lands before that commit, the stale
|
||||
// at-bottom anchor swallows it and the unread counter never increments.
|
||||
// The scroll-to-latest pill only mounts once that state has committed,
|
||||
// so waiting for it guarantees the anchor is in the away-from-bottom
|
||||
// branch before callers send the message they expect to be counted.
|
||||
await expect(page.getByTestId("message-scroll-to-latest")).toBeVisible();
|
||||
return metrics.distanceFromBottom;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Failed to scroll the timeline away from the bottom.");
|
||||
}
|
||||
|
||||
test.beforeAll(async () => {
|
||||
test.setTimeout(relaySeedHookTimeoutMs);
|
||||
await assertRelaySeeded();
|
||||
});
|
||||
|
||||
test("loads channels from the relay", async ({ page }) => {
|
||||
await installRelayBridge(page, "tyler");
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.getByTestId("stream-list")).toContainText("general");
|
||||
await expect(page.getByTestId("stream-list")).toContainText("random");
|
||||
await expect(page.getByTestId("forum-list")).toContainText("watercooler");
|
||||
await expect(page.getByTestId("dm-list")).toContainText("alice-tyler");
|
||||
});
|
||||
|
||||
test("loads the home feed from the relay", async ({ browser }) => {
|
||||
const message = `Relay home mention ${Date.now()}`;
|
||||
const targetContext = await browser.newContext();
|
||||
const senderContext = await browser.newContext();
|
||||
const page = await targetContext.newPage();
|
||||
const senderPage = await senderContext.newPage();
|
||||
|
||||
try {
|
||||
await installRelayBridge(page, "tyler");
|
||||
await installRelayBridge(senderPage, "alice");
|
||||
await page.goto("/");
|
||||
await senderPage.goto("/");
|
||||
|
||||
await expect(page.getByTestId("home-inbox")).toBeVisible();
|
||||
await expect(page.getByTestId("home-inbox-list")).toBeVisible();
|
||||
|
||||
await sendChannelMessage(senderPage, {
|
||||
channelName: "general",
|
||||
content: message,
|
||||
mentionPubkeys: [TEST_IDENTITIES.tyler.pubkey],
|
||||
});
|
||||
|
||||
await expect(page.getByTestId("home-inbox-list")).toContainText(message);
|
||||
await expect(page.getByTestId("home-inbox-detail")).toBeVisible();
|
||||
} finally {
|
||||
await targetContext.close();
|
||||
await senderContext.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("shows sent inbox replies immediately in the inbox detail pane", async ({
|
||||
browser,
|
||||
}) => {
|
||||
const message = `Relay inbox reply target ${Date.now()}`;
|
||||
const reply = `Inbox reply ${Date.now()}`;
|
||||
const targetContext = await browser.newContext();
|
||||
const senderContext = await browser.newContext();
|
||||
const page = await targetContext.newPage();
|
||||
const senderPage = await senderContext.newPage();
|
||||
|
||||
try {
|
||||
await installRelayBridge(page, "tyler");
|
||||
await installRelayBridge(senderPage, "alice");
|
||||
await page.goto("/");
|
||||
await senderPage.goto("/");
|
||||
|
||||
await sendChannelMessage(senderPage, {
|
||||
channelName: "general",
|
||||
content: message,
|
||||
mentionPubkeys: [TEST_IDENTITIES.tyler.pubkey],
|
||||
});
|
||||
|
||||
await page.getByTestId("home-inbox-list").getByText(message).click();
|
||||
await expect(page.getByTestId("home-inbox-detail")).toBeVisible();
|
||||
await expect(page.getByTestId("message-input")).toBeEnabled();
|
||||
|
||||
await page.getByTestId("message-input").fill(reply);
|
||||
await page.getByTestId("send-message").click();
|
||||
|
||||
await expect(page.getByTestId("home-inbox-detail")).toContainText(reply);
|
||||
} finally {
|
||||
await targetContext.close();
|
||||
await senderContext.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("creates a relay-backed stream", async ({ page }) => {
|
||||
const channelName = `desktop-e2e-${Date.now()}`;
|
||||
|
||||
await installRelayBridge(page, "tyler");
|
||||
await page.goto("/");
|
||||
await openCreateChannelDialog(page);
|
||||
await page.getByTestId("create-channel-name").fill(channelName);
|
||||
await page
|
||||
.getByTestId("create-channel-description")
|
||||
.fill("Created from Playwright");
|
||||
await page.getByTestId("create-channel-submit").click();
|
||||
|
||||
await expect(page.getByTestId("stream-list")).toContainText(channelName);
|
||||
await expect(page.getByTestId("chat-title")).toHaveText(channelName);
|
||||
});
|
||||
|
||||
test("sends a message through the real relay", async ({ page }) => {
|
||||
const message = `Integration message ${Date.now()}`;
|
||||
|
||||
await installRelayBridge(page, "tyler");
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
await page.getByTestId("message-input").fill(message);
|
||||
await page.getByTestId("send-message").click();
|
||||
|
||||
await expectTimelineToContain(page, message);
|
||||
});
|
||||
|
||||
test("delivers a message to a second browser context in real time", async ({
|
||||
browser,
|
||||
}: {
|
||||
browser: Browser;
|
||||
}) => {
|
||||
const channelName = `realtime-shared-${Date.now()}`;
|
||||
const contextOne = await browser.newContext();
|
||||
const contextTwo = await browser.newContext();
|
||||
const pageOne = await contextOne.newPage();
|
||||
const pageTwo = await contextTwo.newPage();
|
||||
const message = `Realtime message ${Date.now()}`;
|
||||
|
||||
try {
|
||||
await installRelayBridge(pageOne, "tyler");
|
||||
await installRelayBridge(pageTwo, "alice");
|
||||
|
||||
await pageOne.goto("/");
|
||||
await pageTwo.goto("/");
|
||||
await createAndJoinSharedStream(pageOne, pageTwo, channelName);
|
||||
|
||||
await pageOne.getByTestId("message-input").fill(message);
|
||||
await pageOne.getByTestId("send-message").click();
|
||||
|
||||
await expectTimelineToContain(pageTwo, message);
|
||||
} finally {
|
||||
await contextOne.close();
|
||||
await contextTwo.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("stays pinned to the latest message when new messages arrive at the bottom", async ({
|
||||
browser,
|
||||
}: {
|
||||
browser: Browser;
|
||||
}) => {
|
||||
test.slow();
|
||||
|
||||
const channelName = `pinned-bottom-${Date.now()}`;
|
||||
const contextOne = await browser.newContext();
|
||||
const contextTwo = await browser.newContext();
|
||||
const pageOne = await contextOne.newPage();
|
||||
const pageTwo = await contextTwo.newPage();
|
||||
const prefix = `Pinned scroll ${Date.now()}`;
|
||||
const incomingMessage = `${prefix} incoming`;
|
||||
|
||||
try {
|
||||
await installRelayBridge(pageOne, "tyler");
|
||||
await installRelayBridge(pageTwo, "alice");
|
||||
|
||||
await pageOne.goto("/");
|
||||
await pageTwo.goto("/");
|
||||
await createAndJoinSharedStream(pageOne, pageTwo, channelName, { prefix });
|
||||
await expect
|
||||
.poll(async () => (await getTimelineMetrics(pageTwo)).distanceFromBottom)
|
||||
.toBeLessThan(8);
|
||||
|
||||
await sendChannelMessage(pageOne, {
|
||||
channelName,
|
||||
content: incomingMessage,
|
||||
});
|
||||
|
||||
await expectTimelineToContain(pageTwo, incomingMessage);
|
||||
await expect
|
||||
.poll(async () => (await getTimelineMetrics(pageTwo)).distanceFromBottom)
|
||||
.toBeLessThan(8);
|
||||
await expect(pageTwo.getByTestId("message-scroll-to-latest")).toHaveCount(
|
||||
0,
|
||||
);
|
||||
} finally {
|
||||
await contextOne.close();
|
||||
await contextTwo.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("stays pinned after you send a message and a remote reply arrives right after", async ({
|
||||
browser,
|
||||
}: {
|
||||
browser: Browser;
|
||||
}) => {
|
||||
test.slow();
|
||||
|
||||
const channelName = `reply-shared-${Date.now()}`;
|
||||
const contextOne = await browser.newContext();
|
||||
const contextTwo = await browser.newContext();
|
||||
const pageOne = await contextOne.newPage();
|
||||
const pageTwo = await contextTwo.newPage();
|
||||
const prefix = `Reply after send ${Date.now()}`;
|
||||
const localMessage = `${prefix} local`;
|
||||
const incomingMessage = `${prefix} incoming`;
|
||||
|
||||
try {
|
||||
await installRelayBridge(pageOne, "tyler");
|
||||
await installRelayBridge(pageTwo, "alice");
|
||||
|
||||
await pageOne.goto("/");
|
||||
await pageTwo.goto("/");
|
||||
await createAndJoinSharedStream(pageOne, pageTwo, channelName, { prefix });
|
||||
await expect
|
||||
.poll(async () => (await getTimelineMetrics(pageTwo)).distanceFromBottom)
|
||||
.toBeLessThan(8);
|
||||
|
||||
await pageTwo.getByTestId("message-input").fill(localMessage);
|
||||
await pageTwo.getByTestId("send-message").click();
|
||||
await expectTimelineToContain(pageTwo, localMessage);
|
||||
|
||||
await sendChannelMessage(pageOne, {
|
||||
channelName,
|
||||
content: incomingMessage,
|
||||
});
|
||||
|
||||
await expectTimelineToContain(pageTwo, incomingMessage);
|
||||
await expect
|
||||
.poll(async () => (await getTimelineMetrics(pageTwo)).distanceFromBottom)
|
||||
.toBeLessThan(8);
|
||||
await expect(pageTwo.getByTestId("message-scroll-to-latest")).toHaveCount(
|
||||
0,
|
||||
);
|
||||
} finally {
|
||||
await contextOne.close();
|
||||
await contextTwo.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("keeps bottom-pinned scrolling after the composer grows", async ({
|
||||
browser,
|
||||
}: {
|
||||
browser: Browser;
|
||||
}) => {
|
||||
test.slow();
|
||||
|
||||
const channelName = `composer-shared-${Date.now()}`;
|
||||
const contextOne = await browser.newContext();
|
||||
const contextTwo = await browser.newContext();
|
||||
const pageOne = await contextOne.newPage();
|
||||
const pageTwo = await contextTwo.newPage();
|
||||
const prefix = `Composer pinned ${Date.now()}`;
|
||||
const incomingMessage = `${prefix} incoming`;
|
||||
const receiverInput = pageTwo.getByTestId("message-input");
|
||||
|
||||
try {
|
||||
await installRelayBridge(pageOne, "tyler");
|
||||
await installRelayBridge(pageTwo, "alice");
|
||||
|
||||
await pageOne.goto("/");
|
||||
await pageTwo.goto("/");
|
||||
await createAndJoinSharedStream(pageOne, pageTwo, channelName, { prefix });
|
||||
await expect
|
||||
.poll(async () => (await getTimelineMetrics(pageTwo)).distanceFromBottom)
|
||||
.toBeLessThan(8);
|
||||
|
||||
await receiverInput.fill("Composer pinned line one");
|
||||
await receiverInput.press("Enter");
|
||||
await receiverInput.type("Composer pinned line two");
|
||||
await receiverInput.press("Enter");
|
||||
await receiverInput.type("Composer pinned line three");
|
||||
await receiverInput.press("Enter");
|
||||
await receiverInput.type("Composer pinned line four");
|
||||
|
||||
await expect
|
||||
.poll(async () => (await getTimelineMetrics(pageTwo)).distanceFromBottom)
|
||||
.toBeLessThan(8);
|
||||
|
||||
await sendChannelMessage(pageOne, {
|
||||
channelName,
|
||||
content: incomingMessage,
|
||||
});
|
||||
|
||||
await expectTimelineToContain(pageTwo, incomingMessage);
|
||||
await expect
|
||||
.poll(async () => (await getTimelineMetrics(pageTwo)).distanceFromBottom)
|
||||
.toBeLessThan(8);
|
||||
await expect(pageTwo.getByTestId("message-scroll-to-latest")).toHaveCount(
|
||||
0,
|
||||
);
|
||||
} finally {
|
||||
await contextOne.close();
|
||||
await contextTwo.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("keeps scroll position when new messages arrive above the fold", async ({
|
||||
browser,
|
||||
}: {
|
||||
browser: Browser;
|
||||
}) => {
|
||||
test.slow();
|
||||
|
||||
const channelName = `scroll-shared-${Date.now()}`;
|
||||
const contextOne = await browser.newContext();
|
||||
const contextTwo = await browser.newContext();
|
||||
const pageOne = await contextOne.newPage();
|
||||
const pageTwo = await contextTwo.newPage();
|
||||
const prefix = `Scroll behavior ${Date.now()}`;
|
||||
const incomingMessage = `${prefix} incoming`;
|
||||
|
||||
try {
|
||||
await installRelayBridge(pageOne, "tyler");
|
||||
await installRelayBridge(pageTwo, "alice");
|
||||
|
||||
await pageOne.goto("/");
|
||||
await pageTwo.goto("/");
|
||||
const minAwayDistance = 80;
|
||||
await createAndJoinSharedStream(pageOne, pageTwo, channelName, {
|
||||
prefix,
|
||||
minDistance: 480,
|
||||
});
|
||||
await expect
|
||||
.poll(async () => (await getTimelineMetrics(pageTwo)).distanceFromBottom)
|
||||
.toBeLessThan(8);
|
||||
|
||||
const distanceBeforeDelivery = await scrollTimelineAwayFromBottom(
|
||||
pageTwo,
|
||||
minAwayDistance,
|
||||
);
|
||||
|
||||
await sendChannelMessage(pageOne, {
|
||||
channelName,
|
||||
content: incomingMessage,
|
||||
});
|
||||
|
||||
await expect(pageTwo.getByTestId("message-scroll-to-latest")).toContainText(
|
||||
"1 new message",
|
||||
);
|
||||
await expect
|
||||
.poll(async () => (await getTimelineMetrics(pageTwo)).distanceFromBottom)
|
||||
.toBeGreaterThanOrEqual(distanceBeforeDelivery - 8);
|
||||
|
||||
await pageTwo.getByTestId("message-scroll-to-latest").click();
|
||||
|
||||
await expectTimelineToContain(pageTwo, incomingMessage);
|
||||
await expect
|
||||
.poll(async () => (await getTimelineMetrics(pageTwo)).distanceFromBottom)
|
||||
.toBeLessThan(8);
|
||||
} finally {
|
||||
await contextOne.close();
|
||||
await contextTwo.close();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user