From 3f514a4e0040c573e427db0131cc99614ecaab25 Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Wed, 10 Sep 2025 15:19:19 -0700 Subject: [PATCH] tools: Convert message-screenshot, thread-screenshot to TypeScript. Signed-off-by: Anders Kaseorg --- .../generate-integration-docs-screenshot | 2 +- .../generate-user-messages-screenshot | 4 +- ...ge-screenshot.js => message-screenshot.ts} | 37 +++++++++++----- ...ead-screenshot.js => thread-screenshot.ts} | 44 +++++++++++++------ 4 files changed, 59 insertions(+), 28 deletions(-) rename tools/screenshots/{message-screenshot.js => message-screenshot.ts} (75%) rename tools/screenshots/{thread-screenshot.js => thread-screenshot.ts} (76%) diff --git a/tools/screenshots/generate-integration-docs-screenshot b/tools/screenshots/generate-integration-docs-screenshot index 4f1a005149..8988603295 100755 --- a/tools/screenshots/generate-integration-docs-screenshot +++ b/tools/screenshots/generate-integration-docs-screenshot @@ -234,7 +234,7 @@ def capture_last_message_screenshot(bot: UserProfile, image_path: str) -> None: print(f"No message found for {bot.full_name}") return message_id = str(message.id) - screenshot_script = os.path.join(SCREENSHOTS_DIR, "message-screenshot.js") + screenshot_script = os.path.join(SCREENSHOTS_DIR, "message-screenshot.ts") subprocess.check_call(["node", screenshot_script, message_id, image_path, realm.url]) diff --git a/tools/screenshots/generate-user-messages-screenshot b/tools/screenshots/generate-user-messages-screenshot index 69c1bc9027..95c2336dea 100755 --- a/tools/screenshots/generate-user-messages-screenshot +++ b/tools/screenshots/generate-user-messages-screenshot @@ -211,7 +211,7 @@ def capture_streams_narrow_screenshot( narrow_uri = topic_narrow_url(realm=realm, stream=stream, topic_name=topic) narrow = f"{stream_name}/{topic}" - screenshot_script = os.path.join(SCREENSHOTS_DIR, "thread-screenshot.js") + screenshot_script = os.path.join(SCREENSHOTS_DIR, "thread-screenshot.ts") subprocess.check_call( ["node", screenshot_script, narrow_uri, narrow, str(unread_msg_id), image_path, realm.url] ) @@ -221,7 +221,7 @@ DESCRIPTION = """ Generate screenshots of messages for corporate pages. This script takes a json file with conversation details, and runs -tools/thread-screenshot.js to take cropped screenshots of the +tools/thread-screenshot.ts to take cropped screenshots of the generated messages with puppeteer. Make sure you have the dev environment up and running in a separate diff --git a/tools/screenshots/message-screenshot.js b/tools/screenshots/message-screenshot.ts similarity index 75% rename from tools/screenshots/message-screenshot.js rename to tools/screenshots/message-screenshot.ts index b51e177c45..fefc41bc6c 100644 --- a/tools/screenshots/message-screenshot.js +++ b/tools/screenshots/message-screenshot.ts @@ -1,31 +1,42 @@ /* global $, CSS */ +import * as assert from "node:assert/strict"; import * as fs from "node:fs"; import path from "node:path"; import {parseArgs} from "node:util"; import "css.escape"; import puppeteer from "puppeteer"; +import * as z from "zod/mini"; -const usage = "Usage: message-screenshot.js "; +const usage = "Usage: message-screenshot.ts "; const { values: {help}, - positionals: [messageId, imagePath, realmUrl], + positionals, } = parseArgs({options: {help: {type: "boolean"}}, allowPositionals: true}); if (help) { console.log(usage); process.exit(0); } -if (realmUrl === undefined) { + +const parsed = z + .tuple([ + z.string(), + z.templateLiteral([z.string(), z.enum([".png", ".jpeg", ".webp"])]), + z.url(), + ]) + .safeParse(positionals); +if (!parsed.success) { console.error(usage); process.exit(1); } +const [messageId, imagePath, realmUrl] = parsed.data; console.log(`Capturing screenshot for message ${messageId} to ${imagePath}`); // TODO: Refactor to share code with web/e2e-tests/realm-creation.test.ts -async function run() { +async function run(): Promise { const browser = await puppeteer.launch({ args: [ "--window-size=1400,1024", @@ -35,7 +46,7 @@ async function run() { "--font-render-hinting=none", ], defaultViewport: null, - headless: "new", + headless: true, }); try { const page = await browser.newPage(); @@ -56,23 +67,27 @@ async function run() { waitUntil: "networkidle2", }); // eslint-disable-next-line no-undef - const message_list_id = await page.evaluate(() => zulip_test.current_msg_list.id); + const message_list_id = await page.evaluate(() => zulip_test.current_msg_list?.id); + assert.ok(message_list_id !== undefined); const messageSelector = `#message-row-${message_list_id}-${CSS.escape(messageId)}`; await page.waitForSelector(messageSelector); // remove unread marker and don't select message const marker = `#message-row-${message_list_id}-${CSS.escape(messageId)} .unread_marker`; await page.evaluate((sel) => $(sel).remove(), marker); const messageBox = await page.$(messageSelector); + assert.ok(messageBox !== null); await page.evaluate((msg) => $(msg).removeClass("selected_message"), messageSelector); const messageGroup = await messageBox.$("xpath/.."); + assert.ok(messageGroup !== null); // Compute screenshot area, with some padding around the message group - const clip = {...(await messageGroup.boundingBox())}; - clip.x -= 5; - clip.width += 10; - clip.y += 5; + const box = await messageGroup.boundingBox(); + assert.ok(box !== null); const imageDir = path.dirname(imagePath); await fs.promises.mkdir(imageDir, {recursive: true}); - await page.screenshot({path: imagePath, clip}); + await page.screenshot({ + path: imagePath, + clip: {x: box.x - 5, y: box.y + 5, width: box.width + 10, height: box.height}, + }); } finally { await browser.close(); } diff --git a/tools/screenshots/thread-screenshot.js b/tools/screenshots/thread-screenshot.ts similarity index 76% rename from tools/screenshots/thread-screenshot.js rename to tools/screenshots/thread-screenshot.ts index 3471bedbdc..bf0d7666db 100644 --- a/tools/screenshots/thread-screenshot.js +++ b/tools/screenshots/thread-screenshot.ts @@ -1,32 +1,45 @@ /* global $, CSS */ +import * as assert from "node:assert/strict"; import * as fs from "node:fs"; import path from "node:path"; import {parseArgs} from "node:util"; import "css.escape"; import puppeteer from "puppeteer"; +import * as z from "zod/mini"; const usage = - "Usage: thread-screenshot.js "; + "Usage: thread-screenshot.ts "; const { values: {help}, - positionals: [narrowUri, narrow, messageId, imagePath, realmUrl], + positionals, } = parseArgs({options: {help: {type: "boolean"}}, allowPositionals: true}); if (help) { console.log(usage); process.exit(0); } -if (realmUrl === undefined) { + +const parsed = z + .tuple([ + z.url(), + z.string(), + z.string(), + z.templateLiteral([z.string(), z.enum([".png", ".jpeg", ".webp"])]), + z.url(), + ]) + .safeParse(positionals); +if (!parsed.success) { console.error(usage); process.exit(1); } +const [narrowUri, narrow, messageId, imagePath, realmUrl] = parsed.data; console.log(`Capturing screenshot for ${narrow} to ${imagePath}`); // TODO: Refactor to share code with web/e2e-tests/realm-creation.test.ts -async function run() { +async function run(): Promise { const browser = await puppeteer.launch({ args: [ "--window-size=500,1024", @@ -36,7 +49,7 @@ async function run() { "--font-render-hinting=none", ], defaultViewport: null, - headless: "new", + headless: true, }); try { const page = await browser.newPage(); @@ -55,20 +68,21 @@ async function run() { // Close any banner at the top of the app before taking any screenshots. const top_banner_close_button_selector = ".banner-close-action"; await page.waitForSelector(top_banner_close_button_selector); - page.click(top_banner_close_button_selector); + await page.click(top_banner_close_button_selector); // Navigate to message and capture screenshot - await page.goto(`${narrowUri}`, { + await page.goto(narrowUri, { waitUntil: "networkidle2", }); // eslint-disable-next-line no-undef - const message_list_id = await page.evaluate(() => zulip_test.current_msg_list.id); + const message_list_id = await page.evaluate(() => zulip_test.current_msg_list?.id); + assert.ok(message_list_id !== undefined); const messageListSelector = "#message-lists-container"; await page.waitForSelector(messageListSelector); // remove unread marker and don't select message const marker = `.message-list[data-message-list-id="${CSS.escape( - message_list_id, + `${message_list_id}`, )}"] .unread_marker`; await page.evaluate((sel) => { $(sel).remove(); @@ -78,6 +92,7 @@ async function run() { await page.waitForSelector(messageSelector); const messageListBox = await page.$(messageListSelector); + assert.ok(messageListBox !== null); await page.evaluate((msg) => $(msg).removeClass("selected_message"), messageSelector); // This is done so as to get white background while capturing screenshots. @@ -92,13 +107,14 @@ async function run() { await page.hover(".compose_new_conversation_button"); // Compute screenshot area, with some padding around the message group - const clip = {...(await messageListBox.boundingBox())}; - clip.width -= 70; - clip.y += 10; - clip.height -= 8; + const box = await messageListBox.boundingBox(); + assert.ok(box !== null); const imageDir = path.dirname(imagePath); await fs.promises.mkdir(imageDir, {recursive: true}); - await page.screenshot({path: imagePath, clip}); + await page.screenshot({ + path: imagePath, + clip: {x: box.x, y: box.y + 10, width: box.width - 70, height: box.height - 8}, + }); } finally { await browser.close(); }