Files
upstream-zulip/web/e2e-tests/copy-messages.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

314 lines
12 KiB
TypeScript

import assert from "node:assert/strict";
import type {Page} from "puppeteer";
import * as common from "./lib/common.ts";
type PartialSelectionConfig = {
select_start_message_partially: boolean;
select_end_message_partially: boolean;
start_text_node_offset?: number;
end_text_node_offset?: number;
};
async function copy_messages(
page: Page,
start_message: string,
end_message: string,
partial_selection_config?: PartialSelectionConfig,
): Promise<string[]> {
return await page.evaluate(
(
start_message: string,
end_message: string,
partial_selection_config?: PartialSelectionConfig,
) => {
function get_message_node(message: string): Element {
return [...document.querySelectorAll(".message-list .message_content")].find(
(node) => node.textContent?.trim() === message,
)!;
}
// select messages from start_message to end_message
const selectedRange = document.createRange();
if (partial_selection_config?.select_start_message_partially) {
const offset = partial_selection_config.start_text_node_offset!;
const start_message_text_node =
get_message_node(start_message).querySelector("p")?.firstChild;
if (!(start_message_text_node instanceof Text)) {
throw new TypeError("Expected a Text node");
}
selectedRange.setStart(start_message_text_node, offset);
} else {
selectedRange.setStartBefore(get_message_node(start_message));
}
if (partial_selection_config?.select_end_message_partially) {
const offset = partial_selection_config.end_text_node_offset!;
const end_message_text_node =
get_message_node(end_message).querySelector("p")?.firstChild;
if (!(end_message_text_node instanceof Text)) {
throw new TypeError("Expected a Text node");
}
// For the last message, the offset will be from the end of the message,
// just like how selecting text in the browser would work.
selectedRange.setEnd(end_message_text_node, end_message_text_node.length - offset);
} else {
selectedRange.setEndAfter(get_message_node(end_message));
}
window.getSelection()!.removeAllRanges();
window.getSelection()!.addRange(selectedRange);
// emulate copy event
const clipboard_data = new DataTransfer();
const copy_event = new ClipboardEvent("copy", {
bubbles: true,
cancelable: true,
clipboardData: clipboard_data,
});
document.dispatchEvent(copy_event);
const copied_html = clipboard_data.getData("text/html");
// Convert the copied HTML into separate message strings
const parser = new DOMParser();
const doc = parser.parseFromString(copied_html, "text/html");
// Empty paragraphs are inserted only to separate consecutive
// messages with a blank line; skip them when collecting lines.
return [...doc.body.children]
.map((el) => el.textContent.trim())
.filter((line) => line !== "");
},
start_message,
end_message,
partial_selection_config,
);
}
async function test_copying_first_message_from_topic(page: Page): Promise<void> {
const actual_copied_lines = await copy_messages(page, "copy paste test C", "copy paste test C");
const expected_copied_lines: string[] = [];
assert.deepStrictEqual(actual_copied_lines, expected_copied_lines);
}
async function test_copying_last_message_from_topic(page: Page): Promise<void> {
const actual_copied_lines = await copy_messages(page, "copy paste test E", "copy paste test E");
const expected_copied_lines: string[] = [];
assert.deepStrictEqual(actual_copied_lines, expected_copied_lines);
}
async function test_copying_first_two_messages_from_topic(page: Page): Promise<void> {
const actual_copied_lines = await copy_messages(page, "copy paste test C", "copy paste test D");
const expected_copied_lines = [
"Desdemona:",
"copy paste test C",
"Desdemona:",
"copy paste test D",
];
assert.deepStrictEqual(actual_copied_lines, expected_copied_lines);
}
async function test_copying_all_messages_from_topic(page: Page): Promise<void> {
const actual_copied_lines = await copy_messages(page, "copy paste test C", "copy paste test E");
const expected_copied_lines = [
"Desdemona:",
"copy paste test C",
"Desdemona:",
"copy paste test D",
"Desdemona:",
"copy paste test E",
];
assert.deepStrictEqual(actual_copied_lines, expected_copied_lines);
}
async function test_copying_last_from_prev_first_from_next(page: Page): Promise<void> {
const actual_copied_lines = await copy_messages(page, "copy paste test B", "copy paste test C");
const expected_copied_lines = [
"Verona > copy-paste-topic #1 | Today",
"Desdemona:",
"copy paste test B",
"Verona > copy-paste-topic #2 | Today",
"Desdemona:",
"copy paste test C",
];
assert.deepStrictEqual(actual_copied_lines, expected_copied_lines);
}
async function test_copying_last_from_prev_all_from_next(page: Page): Promise<void> {
const actual_copied_lines = await copy_messages(page, "copy paste test B", "copy paste test E");
const expected_copied_lines = [
"Verona > copy-paste-topic #1 | Today",
"Desdemona:",
"copy paste test B",
"Verona > copy-paste-topic #2 | Today",
"Desdemona:",
"copy paste test C",
"Desdemona:",
"copy paste test D",
"Desdemona:",
"copy paste test E",
];
assert.deepStrictEqual(actual_copied_lines, expected_copied_lines);
}
async function test_copying_all_from_prev_first_from_next(page: Page): Promise<void> {
const actual_copied_lines = await copy_messages(page, "copy paste test A", "copy paste test C");
const expected_copied_lines = [
"Verona > copy-paste-topic #1 | Today",
"Desdemona:",
"copy paste test A",
"Desdemona:",
"copy paste test B",
"Verona > copy-paste-topic #2 | Today",
"Desdemona:",
"copy paste test C",
];
assert.deepStrictEqual(actual_copied_lines, expected_copied_lines);
}
async function test_copying_messages_from_several_topics(page: Page): Promise<void> {
const actual_copied_lines = await copy_messages(page, "copy paste test B", "copy paste test F");
const expected_copied_lines = [
"Verona > copy-paste-topic #1 | Today",
"Desdemona:",
"copy paste test B",
"Verona > copy-paste-topic #2 | Today",
"Desdemona:",
"copy paste test C",
"Desdemona:",
"copy paste test D",
"Desdemona:",
"copy paste test E",
"Verona > copy-paste-topic #3 | Today",
"Desdemona:",
"copy paste test F",
];
assert.deepStrictEqual(actual_copied_lines, expected_copied_lines);
}
async function test_timestamp_clipboard_has_datetime(page: Page): Promise<void> {
// Verify that copying a rendered timestamp injects <span data-datetime> into the
// selection HTML so Chrome's clipboard serializer cannot silently drop the datetime.
const copied_html = await page.evaluate(() => {
const time_el = document.querySelector<HTMLElement>(
'.message-list time[datetime="2026-05-23T17:30:00Z"]',
);
if (!time_el) {
return null;
}
const range = document.createRange();
range.selectNodeContents(time_el);
window.getSelection()!.removeAllRanges();
window.getSelection()!.addRange(range);
// Dispatch copy: copy_handler runs improve_time_selection_range, which
// injects <span data-datetime> into the DOM and expands the range to
// cover the full <time>. For a single-message selection copy_handler
// returns false (browser handles clipboard natively), so the DataTransfer
// stays empty — but the selection range now contains the mutated DOM.
document.dispatchEvent(
new ClipboardEvent("copy", {
bubbles: true,
cancelable: true,
clipboardData: new DataTransfer(),
}),
);
// Serialize the mutated, expanded selection: this is what Chrome writes
// to the clipboard. Even when Chrome strips <time>, <span data-datetime>
// survives as a plain span and the paste handler can recover <time:ISO>.
const div = document.createElement("div");
div.append(window.getSelection()!.getRangeAt(0).cloneContents());
return div.innerHTML;
});
assert.ok(
copied_html?.includes('data-datetime="2026-05-23T17:30:00Z"'),
`Expected data-datetime="2026-05-23T17:30:00Z" in clipboard HTML, got: ${copied_html}`,
);
}
async function test_multiple_message_selection_with_partially_selected_bookend_messages(
page: Page,
): Promise<void> {
const actual_copied_lines = await copy_messages(
page,
"copy paste test B",
"copy paste test F",
{
select_start_message_partially: true,
select_end_message_partially: true,
start_text_node_offset: 5,
end_text_node_offset: 7,
},
);
const expected_copied_lines = [
"Verona > copy-paste-topic #1 | Today",
"Desdemona:",
// w/o partial selection: "copy paste test B",
"...paste test B",
"Verona > copy-paste-topic #2 | Today",
"Desdemona:",
"copy paste test C",
"Desdemona:",
"copy paste test D",
"Desdemona:",
"copy paste test E",
"Verona > copy-paste-topic #3 | Today",
"Desdemona:",
// w/o partial selection: "copy paste test F",
"copy paste...",
];
assert.deepStrictEqual(actual_copied_lines, expected_copied_lines);
}
async function copy_paste_test(page: Page): Promise<void> {
await common.log_in(page);
await common.send_multiple_messages(page, [
{
stream_name: "Verona",
topic: "copy-paste-topic #0",
content: "<time:2026-05-23T17:30:00Z>",
},
{stream_name: "Verona", topic: "copy-paste-topic #1", content: "copy paste test A"},
{stream_name: "Verona", topic: "copy-paste-topic #1", content: "copy paste test B"},
{stream_name: "Verona", topic: "copy-paste-topic #2", content: "copy paste test C"},
{stream_name: "Verona", topic: "copy-paste-topic #2", content: "copy paste test D"},
{stream_name: "Verona", topic: "copy-paste-topic #2", content: "copy paste test E"},
{stream_name: "Verona", topic: "copy-paste-topic #3", content: "copy paste test F"},
{stream_name: "Verona", topic: "copy-paste-topic #3", content: "copy paste test G"},
]);
await page.click("#left-sidebar-navigation-list .top_left_all_messages");
const message_list_id = await common.get_current_msg_list_id(page, true);
await common.check_messages_sent(page, message_list_id, [
["Verona > copy-paste-topic #0", ["Sat, May 23, 2026, 5:30 PM"]],
["Verona > copy-paste-topic #1", ["copy paste test A", "copy paste test B"]],
[
"Verona > copy-paste-topic #2",
["copy paste test C", "copy paste test D", "copy paste test E"],
],
["Verona > copy-paste-topic #3", ["copy paste test F", "copy paste test G"]],
]);
console.log("Messages were sent successfully");
await test_copying_first_message_from_topic(page);
await test_copying_last_message_from_topic(page);
await test_copying_first_two_messages_from_topic(page);
await test_copying_all_messages_from_topic(page);
await test_copying_last_from_prev_first_from_next(page);
await test_copying_last_from_prev_all_from_next(page);
await test_copying_all_from_prev_first_from_next(page);
await test_copying_messages_from_several_topics(page);
await test_timestamp_clipboard_has_datetime(page);
await test_multiple_message_selection_with_partially_selected_bookend_messages(page);
}
await common.run_test(copy_paste_test);