import assert from "node:assert/strict"; import "css.escape"; import path from "node:path"; import timersPromises from "node:timers/promises"; import * as url from "node:url"; import ErrorStackParser from "error-stack-parser"; import type {Browser, ConsoleMessage, ConsoleMessageLocation, ElementHandle, Page} from "puppeteer"; import puppeteer from "puppeteer"; import StackFrame from "stackframe"; import StackTraceGPS from "stacktrace-gps"; import {test_credentials} from "../../../var/puppeteer/test_credentials.js"; const root_dir = url.fileURLToPath(new URL("../../..", import.meta.url)); const puppeteer_dir = path.join(root_dir, "var/puppeteer"); type Message = Record & { recipient?: string; content: string; stream_name?: string; }; let browser: Browser | null = null; let screenshot_id = 0; export const is_firefox = process.env.PUPPETEER_PRODUCT === "firefox"; let realm_url = "http://zulip.zulipdev.com:9981/"; const gps = new StackTraceGPS({ajax: async (url) => (await fetch(url)).text()}); let last_current_msg_list_id: number | undefined; export const pm_recipient = { async set(page: Page, recipient: string): Promise { // Without using the delay option here there seems to be // a flake where the typeahead doesn't show up. await page.type("#private_message_recipient", recipient, {delay: 100}); // PM typeaheads always have an image. This ensures we are waiting for the right typeahead to appear. const entry = await page.waitForSelector(".typeahead .active a .typeahead-image", { visible: false, }); // log entry in puppeteer logs console.log(await entry!.evaluate((el) => el.textContent)); await entry!.click(); }, async expect(page: Page, expected: string): Promise { const actual_recipients = await page.evaluate(() => zulip_test.private_message_recipient()); assert.equal(actual_recipients, expected); }, }; export const fullname = { cordelia: "Cordelia, Lear's daughter", othello: "Othello, the Moor of Venice", hamlet: "King Hamlet", }; export const window_size = { width: 1400, height: 1024, }; export async function ensure_browser(): Promise { if (browser === null) { browser = await puppeteer.launch({ args: [ `--window-size=${window_size.width},${window_size.height}`, "--no-sandbox", "--disable-setuid-sandbox", ], // TODO: Change defaultViewport to 1280x1024 when puppeteer fixes the window size issue with firefox. // Here is link to the issue that is tracking the above problem https://github.com/puppeteer/puppeteer/issues/6442. defaultViewport: null, headless: true, }); } return browser; } export async function get_page(): Promise { const browser = await ensure_browser(); const page = await browser.newPage(); return page; } export async function screenshot(page: Page, name: string | null = null): Promise { if (name === null) { name = `${screenshot_id}`; screenshot_id += 1; } const screenshot_path = path.join(puppeteer_dir, `${name}.png`); await page.screenshot({ path: screenshot_path, }); } export async function page_url_with_fragment(page: Page): Promise { // `page.url()` does not include the url fragment when running // Puppeteer with Firefox: https://github.com/puppeteer/puppeteer/issues/6787. // // This function hacks around that issue; once it's fixed in // puppeteer upstream, we can delete this function and return // its callers to using `page.url()` return await page.evaluate(() => window.location.href); } // This function will clear the existing value of the element and // replace it with the text. export async function clear_and_type(page: Page, selector: string, text: string): Promise { // Select all text currently in the element. await page.click(selector, {clickCount: 3}); await page.keyboard.press("Delete"); await page.type(selector, text); } /** * This function takes a params object whose fields * are referenced by name attribute of an input field and * the input as a key. * * For example to fill: *
* * *
* * You can call: * common.fill_form(page, '#demo', { * username: 'Iago', * terms: true * }); */ export async function fill_form( page: Page, form_selector: string, params: Record, ): Promise { async function is_dropdown(page: Page, name: string): Promise { return (await page.$(`select[name="${CSS.escape(name)}"]`)) !== null; } for (const [name, value] of Object.entries(params)) { if (typeof value === "boolean") { await page.$eval( `${form_selector} input[name="${CSS.escape(name)}"]`, (el, value) => { if (el.checked !== value) { el.click(); } }, value, ); } else if (await is_dropdown(page, name)) { if (typeof value !== "string") { throw new TypeError(`Expected string for ${name}`); } await page.select(`${form_selector} select[name="${CSS.escape(name)}"]`, value); } else { await clear_and_type(page, `${form_selector} [name="${CSS.escape(name)}"]`, value); } } } export async function check_form_contents( page: Page, form_selector: string, params: Record, ): Promise { for (const name of Object.keys(params)) { const expected_value = params[name]; if (typeof expected_value === "boolean") { assert.equal( await page.$eval( `${form_selector} input[name="${CSS.escape(name)}"]`, (el) => el.checked, ), expected_value, "Form content is not as expected.", ); } else { assert.equal( await page.$eval(`${form_selector} [name="${CSS.escape(name)}"]`, (el) => { if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) { throw new TypeError("Expected or