Files
upstream-zulip/web/e2e-tests/mark-messages-read-near.test.ts
upstream-mirror 64898b2c79
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
2026-09-15 15:05:30 +05:00

269 lines
11 KiB
TypeScript

import assert from "node:assert/strict";
import type {Page} from "puppeteer";
import * as common from "./lib/common.ts";
const stream_name = "Verona";
const topic_name = "near-read-test";
const realm_url = "http://zulip.zulipdev.com:9981/";
async function get_stream_id(page: Page): Promise<number> {
const stream_id = await common.get_stream_id(page, stream_name);
assert.ok(stream_id !== undefined);
return stream_id;
}
async function navigate_to_settings_preferences(page: Page): Promise<void> {
await common.open_personal_menu(page);
await page.waitForSelector("#personal-menu-dropdown a[href^='#settings']", {visible: true});
await page.click("#personal-menu-dropdown a[href^='#settings']");
await page.waitForSelector("#settings_overlay_container.show", {visible: true});
await page.waitForSelector('[data-section="preferences"]', {visible: true});
await page.click('[data-section="preferences"]');
await page.waitForSelector("#user_web_mark_read_on_scroll_policy", {visible: true});
}
async function change_mark_read_policy(page: Page, value: string): Promise<void> {
await navigate_to_settings_preferences(page);
await page.select("#user_web_mark_read_on_scroll_policy", value);
await page.waitForSelector("#user-preferences .general-settings-status", {visible: true});
await page.click("#settings_page .content-wrapper .exit");
await page.waitForSelector("#settings_overlay_container", {hidden: true});
}
// Test 1: A /near/ narrow is treated as a conversation view.
async function test_near_narrow_is_conversation_view(page: Page): Promise<void> {
console.log("Testing that near narrow is treated as a conversation view");
await common.send_message(page, "stream", {
stream_name,
topic: topic_name,
content: "message for near narrow test",
});
const stream_id = await get_stream_id(page);
const msg_id = await page.evaluate(() => zulip_test.current_msg_list?.last()?.id);
assert.ok(msg_id !== undefined, "Expected a message to be sent and visible");
// Navigate away from the topic, then come back via /near/ URL.
await page.click("#left-sidebar-navigation-list .top_left_all_messages");
await common.get_current_msg_list_id(page, true);
await page.goto(
`${realm_url}#narrow/channel/${stream_id}-${stream_name}/topic/${topic_name}/near/${msg_id}`,
);
await common.get_current_msg_list_id(page, true);
const is_conversation_view = await page.evaluate(() =>
zulip_test.current_msg_list?.data.filter.is_conversation_view(),
);
assert.ok(is_conversation_view, "Near narrow should be treated as a conversation view");
const is_conversation_view_with_near = await page.evaluate(() =>
zulip_test.current_msg_list?.data.filter.is_conversation_view_with_near(),
);
assert.ok(
is_conversation_view_with_near,
"Near narrow should still identify as is_conversation_view_with_near",
);
}
// Test 2: The reading gate clears immediately when all messages in the
// near narrow are already read, enabling normal mark-as-read behavior.
async function test_near_narrow_reading_gate_clears(page: Page): Promise<void> {
console.log("Testing that reading gate clears when no unread messages in near view");
// We are on the near narrow from test 1; get the last message ID.
const stream_id = await get_stream_id(page);
const msg_id = await page.evaluate(() => zulip_test.current_msg_list?.last()?.id);
assert.ok(msg_id !== undefined);
// Navigate away, then return via /near/ URL. All messages were
// already marked as read in test 1 when we first visited this topic.
await page.click("#left-sidebar-navigation-list .top_left_all_messages");
await common.get_current_msg_list_id(page, true);
await page.goto(
`${realm_url}#narrow/channel/${stream_id}-${stream_name}/topic/${topic_name}/near/${msg_id}`,
);
await common.get_current_msg_list_id(page, true);
// The not_found case fires immediately (no unreads), clearing the gate
// and resuming reading via resume_reading().
await page.waitForFunction(
() => zulip_test.current_msg_list?.near_view_reading_gate_pending === false,
{timeout: 5000},
);
const reading_prevented = await page.evaluate(
() => zulip_test.current_msg_list?.reading_prevented,
);
assert.equal(
reading_prevented,
false,
"reading_prevented should be false after gate clears with no unreads",
);
}
// Test 3: With conversation_only policy, near views don't show the
// "not a conversation view" banner because they're treated as conversation views.
async function test_near_narrow_no_conversation_only_banner(page: Page): Promise<void> {
console.log("Testing banner behavior in near narrow with conversation_only policy");
// We are on the near narrow from test 2.
const stream_id = await get_stream_id(page);
const msg_id = await page.evaluate(() => zulip_test.current_msg_list?.last()?.id);
assert.ok(msg_id !== undefined);
// Change policy to "Only in conversation views".
await change_mark_read_policy(page, "2");
// Navigate to a channel-only narrow (not a conversation view).
// update_unread_banner() always sets the banner HTML on narrow change, so
// verify the banner text contains "conversation" without requiring the banner
// to be visible (which requires an unread message + scroll to trigger).
await page.goto(`${realm_url}#narrow/channel/${stream_id}-${stream_name}`);
await common.get_current_msg_list_id(page, true);
await page.waitForFunction(
() =>
(
document.querySelector("#mark_as_read_turned_off_content")?.textContent ?? ""
).includes("conversation"),
{timeout: 5000},
);
const channel_banner_text = await page.$eval(
"#mark_as_read_turned_off_content",
(el) => el.textContent ?? "",
);
assert.ok(
channel_banner_text.includes("conversation"),
"Channel narrow should use 'not a conversation view' banner template",
);
// Navigate to a near narrow. It should not show the "not a
// conversation view" banner since near views are now conversation views.
await page.goto(
`${realm_url}#narrow/channel/${stream_id}-${stream_name}/topic/${topic_name}/near/${msg_id}`,
);
await common.get_current_msg_list_id(page, true);
// Wait for the reading gate to clear (all messages are read, so
// maybe_resume_reading_for_near_view fires the not_found path).
await page.waitForFunction(
() => zulip_test.current_msg_list?.near_view_reading_gate_pending === false,
{timeout: 5000},
);
const near_banner_text = await page.$eval(
"#mark_as_read_turned_off_content",
(el) => el.textContent ?? "",
);
assert.ok(
!near_banner_text.includes("conversation"),
"Near narrow should not show 'not a conversation view' banner",
);
// Reset the policy to "Always".
await change_mark_read_policy(page, "1");
}
// Test 4: The reading gate only clears once the first unread message
// has scrolled into the visible area of the viewport.
async function test_near_narrow_gate_requires_scroll(page: Page): Promise<void> {
console.log("Testing that gate requires first unread to be visible before marking as read");
const gate_topic = "near-gate-test";
const stream_id = await get_stream_id(page);
// Send 15 messages so that, when anchored at the last one, the first
// message is well above the viewport and the gate cannot clear until
// the user scrolls back to the top.
const msgs = Array.from({length: 15}, (_, i) => ({
stream_name,
topic: gate_topic,
content: `gate test message ${i + 1}`,
}));
await common.send_multiple_messages(page, msgs);
const msg_last_id = await page.evaluate(() => zulip_test.current_msg_list?.last()?.id);
assert.ok(msg_last_id !== undefined);
// Navigate to the combined feed (all sent messages are now marked as read).
await page.click("#left-sidebar-navigation-list .top_left_all_messages");
await common.get_current_msg_list_id(page, true);
// Mark all messages in the topic as unread so the gate has something to gate on.
await page.evaluate(
(sid: number, topic: string) => {
zulip_test.mark_topic_as_unread(sid, topic);
},
stream_id,
gate_topic,
);
// Wait for the client's unread state to reflect the server's response.
await page.waitForFunction(
(sid: number, topic: string) => zulip_test.num_unread_for_topic(sid, topic) > 0,
{timeout: 10000},
stream_id,
gate_topic,
);
// Navigate via /near/ anchored at the last message. The first unread
// (message 1) is above the viewport, so the gate should stay pending.
await page.goto(
`${realm_url}#narrow/channel/${stream_id}-${stream_name}/topic/${gate_topic}/near/${msg_last_id}`,
);
await common.get_current_msg_list_id(page, true);
// Gate must still be pending: the first unread is above the visible area.
const gate_pending = await page.evaluate(
() => zulip_test.current_msg_list?.near_view_reading_gate_pending,
);
assert.ok(gate_pending, "Gate should be pending when first unread is above the viewport");
let reading_prevented = await page.evaluate(
() => zulip_test.current_msg_list?.reading_prevented,
);
assert.equal(
reading_prevented,
true,
"reading_prevented should be false after scrolling first unread into view",
);
// Scroll to the top so the first unread enters the visible area, then
// directly call maybe_resume_reading_for_near_view to check whether
// the gate clears. We call it directly (rather than relying on the
// scroll handler) because the test scroll and the initial render's
// anchor scroll share a debounce timer, causing scroll_finished() to
// fire once with update_selection_on_next_scroll=false and skip the
// gate check. This matches how render_message_list_with_selected_message
// calls the function directly during the initial render.
await page.evaluate(() => {
document.documentElement.scrollTop = 0;
zulip_test.current_msg_list?.maybe_resume_reading_for_near_view();
});
// The gate should now clear since the first unread is visible.
await page.waitForFunction(
() => zulip_test.current_msg_list?.near_view_reading_gate_pending === false,
{timeout: 5000},
);
reading_prevented = await page.evaluate(() => zulip_test.current_msg_list?.reading_prevented);
assert.equal(
reading_prevented,
false,
"reading_prevented should be false after scrolling first unread into view",
);
}
async function mark_messages_read_near_test(page: Page): Promise<void> {
await common.log_in(page);
await page.click("#left-sidebar-navigation-list .top_left_all_messages");
await common.get_current_msg_list_id(page, true);
await test_near_narrow_is_conversation_view(page);
await test_near_narrow_reading_gate_clears(page);
await test_near_narrow_no_conversation_only_banner(page);
await test_near_narrow_gate_requires_scroll(page);
}
await common.run_test(mark_messages_read_near_test);