mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-05-27 09:44:20 +00:00
267 lines
10 KiB
JavaScript
267 lines
10 KiB
JavaScript
const { test, expect } = require("@playwright/test");
|
|
const {
|
|
prepareE2eSession,
|
|
seedE2eLongConversationThread,
|
|
seedE2eAltShortConversationThread,
|
|
getE2eLocalLxmfHash,
|
|
E2E_SCROLL_PEER_HASH,
|
|
} = require("./helpers");
|
|
|
|
async function waitForMessagesViewportReady(page) {
|
|
await page.waitForFunction(
|
|
() => {
|
|
const el = document.getElementById("messages");
|
|
return el != null && el.getAttribute("aria-busy") !== "true";
|
|
},
|
|
null,
|
|
{ timeout: 30000 }
|
|
);
|
|
}
|
|
|
|
async function scrollMetrics(page) {
|
|
await waitForMessagesViewportReady(page);
|
|
const loc = page.locator("#messages");
|
|
return loc.evaluate((el) => ({
|
|
scrollTop: el.scrollTop,
|
|
scrollHeight: el.scrollHeight,
|
|
clientHeight: el.clientHeight,
|
|
}));
|
|
}
|
|
|
|
async function setMessagesToBottom(page) {
|
|
await page.locator("#messages").evaluate((el) => {
|
|
const inner = el.firstElementChild;
|
|
const reverse = inner && getComputedStyle(inner).flexDirection === "column-reverse";
|
|
if (reverse) {
|
|
el.scrollTop = 0;
|
|
return;
|
|
}
|
|
el.scrollTop = Math.max(0, el.scrollHeight - el.clientHeight);
|
|
});
|
|
}
|
|
|
|
async function messagesDistanceFromBottom(page) {
|
|
return page.locator("#messages").evaluate((el) => {
|
|
const inner = el.firstElementChild;
|
|
const reverse = inner && getComputedStyle(inner).flexDirection === "column-reverse";
|
|
const max = Math.max(0, el.scrollHeight - el.clientHeight);
|
|
if (reverse) {
|
|
return el.scrollTop;
|
|
}
|
|
return max - el.scrollTop;
|
|
});
|
|
}
|
|
|
|
async function messagesNearBottom(page) {
|
|
return page.locator("#messages").evaluate((el) => {
|
|
const inner = el.firstElementChild;
|
|
const reverse = inner && getComputedStyle(inner).flexDirection === "column-reverse";
|
|
const max = Math.max(0, el.scrollHeight - el.clientHeight);
|
|
const eps = 12;
|
|
if (reverse) {
|
|
return el.scrollTop <= eps;
|
|
}
|
|
return max - el.scrollTop <= eps;
|
|
});
|
|
}
|
|
|
|
async function waitForMessagesOverflow(page) {
|
|
await page.waitForFunction(
|
|
() => {
|
|
const el = document.getElementById("messages");
|
|
if (!el) {
|
|
return false;
|
|
}
|
|
return el.scrollHeight > el.clientHeight + 100;
|
|
},
|
|
null,
|
|
{ timeout: 30000 }
|
|
);
|
|
}
|
|
|
|
test.describe("Messages conversation scroll", () => {
|
|
test.use({ viewport: { width: 1280, height: 360 } });
|
|
|
|
test.beforeAll(async ({ request }) => {
|
|
await prepareE2eSession(request);
|
|
await seedE2eLongConversationThread(request, { messageCount: 120 });
|
|
await seedE2eAltShortConversationThread(request, { messageCount: 12 });
|
|
});
|
|
|
|
test.beforeEach(async ({ request }) => {
|
|
await prepareE2eSession(request);
|
|
});
|
|
|
|
test("starts near bottom with a long thread", async ({ page }) => {
|
|
await page.goto("/#/messages");
|
|
await expect(page.getByText("Conversations", { exact: true }).first()).toBeVisible({ timeout: 25000 });
|
|
await page
|
|
.locator(".conversation-item")
|
|
.filter({ hasText: /E2E scroll seed/ })
|
|
.first()
|
|
.click();
|
|
await waitForMessagesViewportReady(page);
|
|
await expect(page.locator("#messages")).toBeVisible({ timeout: 25000 });
|
|
await expect(
|
|
page
|
|
.locator("#messages")
|
|
.getByText(/E2E scroll seed 119/)
|
|
.first()
|
|
).toBeVisible({
|
|
timeout: 25000,
|
|
});
|
|
|
|
await waitForMessagesOverflow(page);
|
|
const m = await scrollMetrics(page);
|
|
expect(m.scrollHeight).toBeGreaterThan(m.clientHeight + 80);
|
|
expect(await messagesNearBottom(page)).toBe(true);
|
|
});
|
|
|
|
test("does not jump to bottom when scrolled up and inbound arrives", async ({ page, request }) => {
|
|
const localHash = await getE2eLocalLxmfHash(request);
|
|
await page.goto("/#/messages");
|
|
await expect(page.getByText("Conversations", { exact: true }).first()).toBeVisible({ timeout: 25000 });
|
|
await page
|
|
.locator(".conversation-item")
|
|
.filter({ hasText: /E2E scroll seed/ })
|
|
.first()
|
|
.click();
|
|
await waitForMessagesViewportReady(page);
|
|
await expect(page.locator("#messages")).toBeVisible({ timeout: 25000 });
|
|
await waitForMessagesOverflow(page);
|
|
|
|
await page.locator("#messages").evaluate(() => {
|
|
const el = document.getElementById("messages");
|
|
if (!el) {
|
|
return;
|
|
}
|
|
const max = Math.max(0, el.scrollHeight - el.clientHeight);
|
|
el.scrollTop = Math.max(0, max - 200);
|
|
});
|
|
await page.waitForTimeout(300);
|
|
const before = await scrollMetrics(page);
|
|
expect(await messagesNearBottom(page)).toBe(false);
|
|
|
|
await page.evaluate(
|
|
({ peerHash, localHash: lh }) => {
|
|
const buf = new Uint8Array(16);
|
|
window.crypto.getRandomValues(buf);
|
|
const hash = Array.from(buf, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
return import("/js/WebSocketConnection.js").then((mod) => {
|
|
mod.default.emit("message", {
|
|
data: JSON.stringify({
|
|
type: "lxmf.delivery",
|
|
lxmf_message: {
|
|
hash,
|
|
source_hash: peerHash,
|
|
destination_hash: lh,
|
|
content: "E2E synthetic inbound (scroll up)",
|
|
timestamp: Math.floor(Date.now() / 1000),
|
|
},
|
|
}),
|
|
});
|
|
});
|
|
},
|
|
{ peerHash: E2E_SCROLL_PEER_HASH, localHash }
|
|
);
|
|
|
|
await page.waitForTimeout(500);
|
|
expect(await messagesNearBottom(page)).toBe(false);
|
|
});
|
|
|
|
test("stays pinned to bottom when new inbound arrives while at bottom", async ({ page, request }) => {
|
|
const localHash = await getE2eLocalLxmfHash(request);
|
|
await page.goto("/#/messages");
|
|
await expect(page.getByText("Conversations", { exact: true }).first()).toBeVisible({ timeout: 25000 });
|
|
await page
|
|
.locator(".conversation-item")
|
|
.filter({ hasText: /E2E scroll seed/ })
|
|
.first()
|
|
.click();
|
|
await waitForMessagesViewportReady(page);
|
|
await expect(page.locator("#messages")).toBeVisible({ timeout: 25000 });
|
|
await waitForMessagesOverflow(page);
|
|
|
|
await setMessagesToBottom(page);
|
|
await page.waitForTimeout(200);
|
|
expect(await messagesNearBottom(page)).toBe(true);
|
|
|
|
await page.evaluate(
|
|
({ peerHash, localHash: lh }) => {
|
|
const buf = new Uint8Array(16);
|
|
window.crypto.getRandomValues(buf);
|
|
const hash = Array.from(buf, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
return import("/js/WebSocketConnection.js").then((mod) => {
|
|
mod.default.emit("message", {
|
|
data: JSON.stringify({
|
|
type: "lxmf.delivery",
|
|
lxmf_message: {
|
|
hash,
|
|
source_hash: peerHash,
|
|
destination_hash: lh,
|
|
content: "E2E synthetic inbound (at bottom)",
|
|
timestamp: Math.floor(Date.now() / 1000),
|
|
},
|
|
}),
|
|
});
|
|
});
|
|
},
|
|
{ peerHash: E2E_SCROLL_PEER_HASH, localHash }
|
|
);
|
|
|
|
await page.waitForTimeout(600);
|
|
expect(await messagesNearBottom(page)).toBe(true);
|
|
});
|
|
|
|
test("stays near bottom when switching between long and short threads repeatedly", async ({ page }) => {
|
|
await page.goto("/#/messages");
|
|
await expect(page.getByText("Conversations", { exact: true }).first()).toBeVisible({ timeout: 25000 });
|
|
const longRow = page
|
|
.locator(".conversation-item")
|
|
.filter({ hasText: /E2E scroll seed/ })
|
|
.first();
|
|
const shortRow = page
|
|
.locator(".conversation-item")
|
|
.filter({ hasText: /E2E alt short/ })
|
|
.first();
|
|
for (let i = 0; i < 5; i++) {
|
|
await longRow.click();
|
|
await waitForMessagesViewportReady(page);
|
|
await expect(page.locator("#messages")).toBeVisible({ timeout: 25000 });
|
|
await waitForMessagesOverflow(page);
|
|
expect(await messagesNearBottom(page)).toBe(true);
|
|
|
|
await shortRow.click();
|
|
await waitForMessagesViewportReady(page);
|
|
await expect(page.locator("#messages")).toBeVisible({ timeout: 25000 });
|
|
expect(await messagesNearBottom(page)).toBe(true);
|
|
}
|
|
});
|
|
|
|
test("preserves scroll anchor when loading older messages from the top", async ({ page }) => {
|
|
await page.goto("/#/messages");
|
|
await expect(page.getByText("Conversations", { exact: true }).first()).toBeVisible({ timeout: 25000 });
|
|
await page
|
|
.locator(".conversation-item")
|
|
.filter({ hasText: /E2E scroll seed/ })
|
|
.first()
|
|
.click();
|
|
await waitForMessagesViewportReady(page);
|
|
await expect(page.locator("#messages")).toBeVisible({ timeout: 25000 });
|
|
await waitForMessagesOverflow(page);
|
|
|
|
const initial = await scrollMetrics(page);
|
|
expect(initial.scrollHeight).toBeGreaterThan(initial.clientHeight + 80);
|
|
|
|
await page.locator("#messages").hover();
|
|
for (let i = 0; i < 60; i++) {
|
|
await page.mouse.wheel(0, -500);
|
|
}
|
|
await page.waitForTimeout(2500);
|
|
const loaded = await scrollMetrics(page);
|
|
if (loaded.scrollHeight > initial.scrollHeight) {
|
|
expect(await messagesDistanceFromBottom(page)).toBeGreaterThan(8);
|
|
}
|
|
});
|
|
});
|