From 279c4b0e24a2e5b079c5b6cbc7ee06dd4fa9fd41 Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Fri, 19 Feb 2021 20:52:06 -0800 Subject: [PATCH] puppeteer_tests: Port to TypeScript. Signed-off-by: Anders Kaseorg --- .eslintignore | 5 +- .gitignore | 5 +- .../puppeteer_lib/{common.js => common.ts} | 154 ++++++++++-------- ...realm-creation.js => 00-realm-creation.ts} | 22 +-- frontend_tests/puppeteer_tests/01-login.js | 11 -- frontend_tests/puppeteer_tests/01-login.ts | 11 ++ ...message-basics.js => 02-message-basics.ts} | 70 ++++---- .../{03-compose.js => 03-compose.ts} | 58 +++---- ...4-subscriptions.js => 04-subscriptions.ts} | 91 ++++++----- .../{05-stars.js => 05-stars.ts} | 16 +- .../{06-edit.js => 06-edit.ts} | 16 +- .../{07-navigation.js => 07-navigation.ts} | 20 +-- .../{08-admin.js => 08-admin.ts} | 100 +++++++----- .../{09-mention.js => 09-mention.ts} | 8 +- ...custom-profile.js => 10-custom-profile.ts} | 14 +- ...eactivation.js => 11-user-deactivation.ts} | 22 +-- .../{12-drafts.js => 12-drafts.ts} | 40 ++--- ...delete-message.js => 13-delete-message.ts} | 12 +- ...copy-and-paste.js => 14-copy-and-paste.ts} | 44 ++--- ...alm-linkifier.js => 15-realm-linkifier.ts} | 14 +- .../{16-settings.js => 16-settings.ts} | 74 +++++---- static/js/global.d.ts | 4 + tools/lib/test_script.py | 4 +- tools/test-js-with-puppeteer | 9 +- var/puppeteer/test_credentials.d.ts | 6 + 25 files changed, 461 insertions(+), 369 deletions(-) rename frontend_tests/puppeteer_lib/{common.js => common.ts} (76%) rename frontend_tests/puppeteer_tests/{00-realm-creation.js => 00-realm-creation.ts} (77%) delete mode 100644 frontend_tests/puppeteer_tests/01-login.js create mode 100644 frontend_tests/puppeteer_tests/01-login.ts rename frontend_tests/puppeteer_tests/{02-message-basics.js => 02-message-basics.ts} (86%) rename frontend_tests/puppeteer_tests/{03-compose.js => 03-compose.ts} (80%) rename frontend_tests/puppeteer_tests/{04-subscriptions.js => 04-subscriptions.ts} (77%) rename frontend_tests/puppeteer_tests/{05-stars.js => 05-stars.ts} (79%) rename frontend_tests/puppeteer_tests/{06-edit.js => 06-edit.ts} (82%) rename frontend_tests/puppeteer_tests/{07-navigation.js => 07-navigation.ts} (84%) rename frontend_tests/puppeteer_tests/{08-admin.js => 08-admin.ts} (81%) rename frontend_tests/puppeteer_tests/{09-mention.js => 09-mention.ts} (89%) rename frontend_tests/puppeteer_tests/{10-custom-profile.js => 10-custom-profile.ts} (85%) rename frontend_tests/puppeteer_tests/{11-user-deactivation.js => 11-user-deactivation.ts} (84%) rename frontend_tests/puppeteer_tests/{12-drafts.js => 12-drafts.ts} (87%) rename frontend_tests/puppeteer_tests/{13-delete-message.js => 13-delete-message.ts} (70%) rename frontend_tests/puppeteer_tests/{14-copy-and-paste.js => 14-copy-and-paste.ts} (80%) rename frontend_tests/puppeteer_tests/{15-realm-linkifier.js => 15-realm-linkifier.ts} (84%) rename frontend_tests/puppeteer_tests/{16-settings.js => 16-settings.ts} (84%) create mode 100644 var/puppeteer/test_credentials.d.ts diff --git a/.eslintignore b/.eslintignore index faa565f821..59541c8a5d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -6,5 +6,8 @@ /static/generated /static/third /static/webpack-bundles -/var +/var/* +!/var/puppeteer +/var/puppeteer/* +!/var/puppeteer/test_credentials.d.ts /zulip-py3-venv diff --git a/.gitignore b/.gitignore index a071e20bc6..540c356a04 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,10 @@ package-lock.json /.vagrant -/var +/var/* +!/var/puppeteer +/var/puppeteer/* +!/var/puppeteer/test_credentials.d.ts /.dmypy.json diff --git a/frontend_tests/puppeteer_lib/common.js b/frontend_tests/puppeteer_lib/common.ts similarity index 76% rename from frontend_tests/puppeteer_lib/common.js rename to frontend_tests/puppeteer_lib/common.ts index 48625ac869..7e066a4fa1 100644 --- a/frontend_tests/puppeteer_lib/common.js +++ b/frontend_tests/puppeteer_lib/common.ts @@ -1,22 +1,22 @@ -"use strict"; +import {strict as assert} from "assert"; +import "css.escape"; +import path from "path"; -const {strict: assert} = require("assert"); -const path = require("path"); +import {Browser, Page, launch} from "puppeteer"; -require("css.escape"); -const puppeteer = require("puppeteer"); - -const {test_credentials} = require("../../var/puppeteer/test_credentials"); +import {test_credentials} from "../../var/puppeteer/test_credentials"; const root_dir = path.resolve(__dirname, "../../"); const puppeteer_dir = path.join(root_dir, "var/puppeteer"); +type Message = Record & {recipient?: string; content: string}; + class CommonUtils { - browser = null; + browser: Browser | null = null; screenshot_id = 0; realm_url = "http://zulip.zulipdev.com:9981/"; pm_recipient = { - async set(page, 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}); @@ -37,7 +37,7 @@ class CommonUtils { }); }, - async expect(page, expected) { + async expect(page: Page, expected: string): Promise { const actual_recipients = await page.evaluate(() => { const compose_state = window.require("./static/js/compose_state"); return compose_state.private_message_recipient(); @@ -46,7 +46,7 @@ class CommonUtils { }, }; - fullname = { + fullname: Record = { cordelia: "Cordelia Lear", othello: "Othello, the Moor of Venice", hamlet: "King Hamlet", @@ -57,10 +57,10 @@ class CommonUtils { height: 1024, }; - async ensure_browser() { + async ensure_browser(): Promise { if (this.browser === null) { const {window_size} = this; - this.browser = await puppeteer.launch({ + this.browser = await launch({ args: [ `--window-size=${window_size.width},${window_size.height}`, "--no-sandbox", @@ -70,15 +70,16 @@ class CommonUtils { headless: true, }); } + return this.browser; } - async get_page() { - await this.ensure_browser(); - const page = await this.browser.newPage(); + async get_page(): Promise { + const browser = await this.ensure_browser(); + const page = await browser.newPage(); return page; } - async screenshot(page, name = null) { + async screenshot(page: Page, name: string | null = null): Promise { if (name === null) { name = `${this.screenshot_id}`; this.screenshot_id += 1; @@ -107,8 +108,12 @@ class CommonUtils { * terms: true * }); */ - async fill_form(page, form_selector, params) { - async function is_dropdown(page, name) { + async fill_form( + page: Page, + form_selector: string, + params: Record, + ): Promise { + async function is_dropdown(page: Page, name: string): Promise { return (await page.$(`select[name="${name}"]`)) !== null; } for (const name of Object.keys(params)) { @@ -118,37 +123,44 @@ class CommonUtils { await page.$eval( name_selector, (el, value) => { - if (el.checked !== value) { + if (el instanceof HTMLInputElement && el.checked !== value) { el.click(); } }, value, ); } else if (await is_dropdown(page, name)) { - await page.select(name_selector, params[name]); + if (typeof value !== "string") { + throw new TypeError(`Expected string for ${name}`); + } + await page.select(name_selector, value); } else { // clear any existing text in the input field before filling. await page.$eval(name_selector, (el) => { - el.value = ""; + (el as HTMLInputElement).value = ""; }); - await page.type(name_selector, params[name]); + await page.type(name_selector, value); } } } - async check_form_contents(page, form_selector, params) { + async check_form_contents( + page: Page, + form_selector: string, + params: Record, + ): Promise { for (const name of Object.keys(params)) { const name_selector = `${form_selector} [name="${name}"]`; const expected_value = params[name]; if (typeof expected_value === "boolean") { assert.equal( - await page.$eval(name_selector, (el) => el.checked), + await page.$eval(name_selector, (el) => (el as HTMLInputElement).checked), expected_value, "Form content is not as expected.", ); } else { assert.equal( - await page.$eval(name_selector, (el) => el.value), + await page.$eval(name_selector, (el) => (el as HTMLInputElement).value), expected_value, "Form content is not as expected.", ); @@ -156,48 +168,51 @@ class CommonUtils { } } - async get_text_from_selector(page, selector) { - return await page.evaluate((selector) => $(selector).text().trim(), selector); + async get_text_from_selector(page: Page, selector: string): Promise { + return await page.evaluate((selector: string) => $(selector).text().trim(), selector); } - async wait_for_text(page, selector, text) { + async wait_for_text(page: Page, selector: string, text: string): Promise { await page.waitForFunction( - (selector, text) => $(selector).text().includes(text), + (selector: string, text: string) => $(selector).text().includes(text), {}, selector, text, ); } - async get_stream_id(page, stream_name) { - return await page.evaluate((stream_name) => { + async get_stream_id(page: Page, stream_name: string): Promise { + return await page.evaluate((stream_name: string) => { const stream_data = window.require("./static/js/stream_data"); return stream_data.get_stream_id(stream_name); }, stream_name); } - async get_user_id_from_name(page, name) { + async get_user_id_from_name(page: Page, name: string): Promise { if (this.fullname[name] !== undefined) { name = this.fullname[name]; } - return await page.evaluate((name) => { - const people = require("./static/js/people"); + return await page.evaluate((name: string) => { + const people = window.require("./static/js/people"); return people.get_user_id_from_name(name); }, name); } - async get_internal_email_from_name(page, name) { + async get_internal_email_from_name(page: Page, name: string): Promise { if (this.fullname[name] !== undefined) { name = this.fullname[name]; } - return await page.evaluate((fullname) => { - const people = require("./static/js/people"); + return await page.evaluate((fullname: string) => { + const people = window.require("./static/js/people"); const user_id = people.get_user_id_from_name(fullname); return people.get_by_user_id(user_id).email; }, name); } - async log_in(page, credentials = null) { + async log_in( + page: Page, + credentials: {username: string; password: string} | null = null, + ): Promise { console.log("Logging in"); await page.goto(this.realm_url + "login/"); assert.equal(this.realm_url + "login/", page.url()); @@ -210,12 +225,12 @@ class CommonUtils { password: credentials.password, }; await this.fill_form(page, "#login_form", params); - await page.$eval("#login_form", (form) => form.submit()); + await page.$eval("#login_form", (form) => (form as HTMLFormElement).submit()); await page.waitForSelector("#zhome .message_row", {visible: true}); } - async log_out(page) { + async log_out(page: Page): Promise { await page.goto(this.realm_url); const menu_selector = "#settings-dropdown"; const logout_selector = 'a[href="#logout"]'; @@ -231,20 +246,20 @@ class CommonUtils { assert(page.url().includes("/login/")); } - async ensure_enter_does_not_send(page) { + async ensure_enter_does_not_send(page: Page): Promise { await page.$eval("#enter_sends", (el) => { - if (el.checked) { - el.click(); + if ((el as HTMLInputElement).checked) { + (el as HTMLInputElement).click(); } }); } - async assert_compose_box_content(page, expected_value) { + async assert_compose_box_content(page: Page, expected_value: string): Promise { await page.waitForSelector("#compose-textarea"); const compose_box_element = await page.$("#compose-textarea"); const compose_box_content = await page.evaluate( - (element) => element.value, + (element: HTMLTextAreaElement) => element.value, compose_box_element, ); assert.equal( @@ -254,9 +269,9 @@ class CommonUtils { ); } - async wait_for_fully_processed_message(page, content) { + async wait_for_fully_processed_message(page: Page, content: string): Promise { await page.waitForFunction( - (content) => { + (content: string) => { /* The tricky part about making sure that a message has actually been fully processed @@ -312,7 +327,7 @@ class CommonUtils { } // Wait for any previous send to finish, then send a message. - async send_message(page, type, params) { + async send_message(page: Page, type: "stream" | "private", params: Message): Promise { // If a message is outside the view, we do not need // to wait for it to be processed later. const outside_view = params.outside_view; @@ -325,7 +340,7 @@ class CommonUtils { await page.keyboard.press("KeyC"); } else if (type === "private") { await page.keyboard.press("KeyX"); - const recipients = params.recipient.split(", "); + const recipients = params.recipient!.split(", "); for (const recipient of recipients) { await this.pm_recipient.set(page, recipient); } @@ -366,7 +381,7 @@ class CommonUtils { await page.waitForSelector("#compose-textarea", {hidden: true}); } - async send_multiple_messages(page, msgs) { + async send_multiple_messages(page: Page, msgs: Message[]): Promise { for (const msg of msgs) { await this.send_message(page, msg.stream !== undefined ? "stream" : "private", msg); } @@ -381,10 +396,10 @@ class CommonUtils { * * The messages are sorted chronologically. */ - async get_rendered_messages(page, table = "zhome") { - return await page.evaluate((table) => { + async get_rendered_messages(page: Page, table = "zhome"): Promise<[string, string[]][]> { + return await page.evaluate((table: string) => { const $recipient_rows = $(`#${CSS.escape(table)}`).find(".recipient_row"); - return $recipient_rows.toArray().map((element) => { + return $recipient_rows.toArray().map((element): [string, string[]] => { const $el = $(element); const stream_name = $el.find(".stream_label").text().trim(); const topic_name = $el.find(".stream_topic a").text().trim(); @@ -399,7 +414,7 @@ class CommonUtils { const messages = $el .find(".message_row .message_content") .toArray() - .map((message_row) => message_row.textContent.trim()); + .map((message_row) => message_row.textContent!.trim()); return [key, messages]; }); @@ -411,7 +426,11 @@ class CommonUtils { // message is { "stream > topic": [messages] }. // The method will only check that all the messages in the // messages array passed exist in the order they are passed. - async check_messages_sent(page, table, messages) { + async check_messages_sent( + page: Page, + table: string, + messages: [string, string[]][], + ): Promise { await page.waitForSelector(`#${CSS.escape(table)}`, {visible: true}); const rendered_messages = await this.get_rendered_messages(page, table); @@ -421,7 +440,7 @@ class CommonUtils { assert.deepStrictEqual(last_n_messages, messages); } - async manage_organization(page) { + async manage_organization(page: Page): Promise { const menu_selector = "#settings-dropdown"; await page.waitForSelector(menu_selector, {visible: true}); await page.click(menu_selector); @@ -437,10 +456,15 @@ class CommonUtils { await page.click(organization_settings_data_section); } - async select_item_via_typeahead(page, field_selector, str, item) { + async select_item_via_typeahead( + page: Page, + field_selector: string, + str: string, + item: string, + ): Promise { console.log(`Looking in ${field_selector} to select ${str}, ${item}`); await page.evaluate( - (field_selector, str, item) => { + (field_selector: string, str: string, item: string) => { // Set the value and then send a bogus keyup event to trigger // the typeahead. $(field_selector) @@ -464,26 +488,26 @@ class CommonUtils { ); } - async run_test(test_function) { + async run_test(test_function: (page: Page) => Promise): Promise { // Pass a page instance to test so we can take // a screenshot of it when the test fails. + const browser = await this.ensure_browser(); const page = await this.get_page(); try { await test_function(page); - } catch (error) { + } catch (error: unknown) { console.log(error); // Take a screenshot, and increment the screenshot_id. await this.screenshot(page, `failure-${this.screenshot_id}`); this.screenshot_id += 1; - await this.browser.close(); + await browser.close(); process.exit(1); } finally { - this.browser.close(); + await browser.close(); } } } -const common = new CommonUtils(); -module.exports = common; +export default new CommonUtils(); diff --git a/frontend_tests/puppeteer_tests/00-realm-creation.js b/frontend_tests/puppeteer_tests/00-realm-creation.ts similarity index 77% rename from frontend_tests/puppeteer_tests/00-realm-creation.js rename to frontend_tests/puppeteer_tests/00-realm-creation.ts index 6f914e8f91..1082903e65 100644 --- a/frontend_tests/puppeteer_tests/00-realm-creation.js +++ b/frontend_tests/puppeteer_tests/00-realm-creation.ts @@ -1,15 +1,15 @@ -"use strict"; +import {strict as assert} from "assert"; -const {strict: assert} = require("assert"); +import type {Page} from "puppeteer"; -const common = require("../puppeteer_lib/common"); +import common from "../puppeteer_lib/common"; const email = "alice@test.example.com"; const subdomain = "testsubdomain"; const organization_name = "Awesome Organization"; const host = "zulipdev.com:9981"; -async function realm_creation_tests(page) { +async function realm_creation_tests(page: Page): Promise { await page.goto("http://" + host + "/new/"); // submit the email for realm creation. @@ -17,7 +17,7 @@ async function realm_creation_tests(page) { await page.type("#email", email); await Promise.all([ page.waitForNavigation(), - page.$eval("#send_confirm", (form) => form.submit()), + page.$eval("#send_confirm", (form) => (form as HTMLFormElement).submit()), ]); // Make sure onfirmation email is sent. @@ -27,8 +27,8 @@ async function realm_creation_tests(page) { await page.goto("http://" + host + "/confirmation_key/"); // Open the confirmation URL - const page_content = await page.evaluate(() => document.querySelector("body").textContent); - const confirmation_key = await JSON.parse(page_content).confirmation_key; + const page_content = await page.evaluate(() => document.querySelector("body")!.textContent); + const confirmation_key = await JSON.parse(page_content!).confirmation_key; const confirmation_url = "http://" + host + "/accounts/do_confirm/" + confirmation_key; await page.goto(confirmation_url); @@ -43,7 +43,9 @@ async function realm_creation_tests(page) { // Make sure the realm creation page is loaded correctly by // checking the text in

tag under pitch class is as expected. await page.waitForSelector(".pitch"); - const text_in_pitch = await page.evaluate(() => document.querySelector(".pitch p").textContent); + const text_in_pitch = await page.evaluate( + () => document.querySelector(".pitch p")!.textContent, + ); assert(text_in_pitch === "You’re almost there! We just need you to do one last thing."); // fill the form. @@ -56,9 +58,9 @@ async function realm_creation_tests(page) { }; // For some reason, page.click() does not work this for particular checkbox // so use page.$eval here to call the .click method in the browser. - await page.$eval("#realm_in_root_domain", (el) => el.click()); + await page.$eval("#realm_in_root_domain", (el) => (el as HTMLInputElement).click()); await common.fill_form(page, "#registration", params); - await page.$eval("#registration", (form) => form.submit()); + await page.$eval("#registration", (form) => (form as HTMLFormElement).submit()); // Check if realm is created and user is logged in by checking if // element of id `lightbox_overlay` exists. diff --git a/frontend_tests/puppeteer_tests/01-login.js b/frontend_tests/puppeteer_tests/01-login.js deleted file mode 100644 index 1fc5665d02..0000000000 --- a/frontend_tests/puppeteer_tests/01-login.js +++ /dev/null @@ -1,11 +0,0 @@ -"use strict"; - -const {test_credentials} = require("../../var/puppeteer/test_credentials"); -const common = require("../puppeteer_lib/common"); - -async function login_tests(page) { - await common.log_in(page, test_credentials.default_user); - await common.log_out(page); -} - -common.run_test(login_tests); diff --git a/frontend_tests/puppeteer_tests/01-login.ts b/frontend_tests/puppeteer_tests/01-login.ts new file mode 100644 index 0000000000..e87a792193 --- /dev/null +++ b/frontend_tests/puppeteer_tests/01-login.ts @@ -0,0 +1,11 @@ +import type {Page} from "puppeteer"; + +import {test_credentials} from "../../var/puppeteer/test_credentials"; +import common from "../puppeteer_lib/common"; + +async function login_tests(page: Page): Promise { + await common.log_in(page, test_credentials.default_user); + await common.log_out(page); +} + +common.run_test(login_tests); diff --git a/frontend_tests/puppeteer_tests/02-message-basics.js b/frontend_tests/puppeteer_tests/02-message-basics.ts similarity index 86% rename from frontend_tests/puppeteer_tests/02-message-basics.js rename to frontend_tests/puppeteer_tests/02-message-basics.ts index 469994bd7c..9f3fd56ebf 100644 --- a/frontend_tests/puppeteer_tests/02-message-basics.js +++ b/frontend_tests/puppeteer_tests/02-message-basics.ts @@ -1,15 +1,15 @@ -"use strict"; +import {strict as assert} from "assert"; -const {strict: assert} = require("assert"); +import type {Page} from "puppeteer"; -const common = require("../puppeteer_lib/common"); +import common from "../puppeteer_lib/common"; -async function get_stream_li(page, stream_name) { +async function get_stream_li(page: Page, stream_name: string): Promise { const stream_id = await common.get_stream_id(page, stream_name); - return `#stream_filters [data-stream-id="${CSS.escape(stream_id)}"]`; + return `#stream_filters [data-stream-id="${CSS.escape(stream_id.toString())}"]`; } -async function expect_home(page) { +async function expect_home(page: Page): Promise { await common.check_messages_sent(page, "zhome", [ ["Verona > test", ["verona test a", "verona test b"]], ["Verona > other topic", ["verona other topic c"]], @@ -22,7 +22,7 @@ async function expect_home(page) { ]); } -async function expect_verona_stream(page) { +async function expect_verona_stream(page: Page): Promise { await page.waitForSelector("#zfilt", {visible: true}); await common.check_messages_sent(page, "zfilt", [ ["Verona > test", ["verona test a", "verona test b"]], @@ -32,7 +32,7 @@ async function expect_verona_stream(page) { assert.strictEqual(await page.title(), "Verona - Zulip Dev - Zulip"); } -async function expect_verona_stream_test_topic(page) { +async function expect_verona_stream_test_topic(page: Page): Promise { await page.waitForSelector("#zfilt", {visible: true}); await common.check_messages_sent(page, "zfilt", [ ["Verona > test", ["verona test a", "verona test b", "verona test d"]], @@ -43,14 +43,14 @@ async function expect_verona_stream_test_topic(page) { ); } -async function expect_verona_other_topic(page) { +async function expect_verona_other_topic(page: Page): Promise { await page.waitForSelector("#zfilt", {visible: true}); await common.check_messages_sent(page, "zfilt", [ ["Verona > other topic", ["verona other topic c"]], ]); } -async function expect_test_topic(page) { +async function expect_test_topic(page: Page): Promise { await page.waitForSelector("#zfilt", {visible: true}); await common.check_messages_sent(page, "zfilt", [ ["Verona > test", ["verona test a", "verona test b"]], @@ -59,7 +59,7 @@ async function expect_test_topic(page) { ]); } -async function expect_huddle(page) { +async function expect_huddle(page: Page): Promise { await page.waitForSelector("#zfilt", {visible: true}); await common.check_messages_sent(page, "zfilt", [ ["You and Cordelia Lear, King Hamlet", ["group pm a", "group pm b", "group pm d"]], @@ -67,12 +67,12 @@ async function expect_huddle(page) { assert.strictEqual(await page.title(), "Cordelia Lear, King Hamlet - Zulip Dev - Zulip"); } -async function expect_cordelia_private_narrow(page) { +async function expect_cordelia_private_narrow(page: Page): Promise { await page.waitForSelector("#zfilt", {visible: true}); await common.check_messages_sent(page, "zfilt", [["You and Cordelia Lear", ["pm c", "pm e"]]]); } -async function un_narrow(page) { +async function un_narrow(page: Page): Promise { if (await page.evaluate(() => $(".message_comp").is(":visible"))) { await page.keyboard.press("Escape"); } @@ -81,11 +81,11 @@ async function un_narrow(page) { assert.strictEqual(await page.title(), "home - Zulip Dev - Zulip"); } -async function un_narrow_by_clicking_org_icon(page) { +async function un_narrow_by_clicking_org_icon(page: Page): Promise { await page.click(".brand"); } -async function test_navigations_from_home(page) { +async function test_navigations_from_home(page: Page): Promise { console.log("Narrowing by clicking stream"); await page.evaluate(() => $(`*[title='Narrow to stream "Verona"']`).trigger("click")); await expect_verona_stream(page); @@ -113,7 +113,13 @@ async function test_navigations_from_home(page) { await expect_home(page); } -async function search_and_check(page, search_str, item_to_select, check, expected_narrow_title) { +async function search_and_check( + page: Page, + search_str: string, + item_to_select: string, + check: (page: Page) => Promise, + expected_narrow_title: string, +): Promise { await common.select_item_via_typeahead(page, "#search_query", search_str, item_to_select); await check(page); assert.strictEqual(await page.title(), expected_narrow_title); @@ -121,7 +127,7 @@ async function search_and_check(page, search_str, item_to_select, check, expecte await expect_home(page); } -async function search_silent_user(page, str, item) { +async function search_silent_user(page: Page, str: string, item: string): Promise { await common.select_item_via_typeahead(page, "#search_query", str, item); await page.waitForSelector("#silent_user", {visible: true}); const expect_message = "You haven't received any messages sent by this user yet!"; @@ -129,7 +135,7 @@ async function search_silent_user(page, str, item) { await un_narrow(page); } -async function expect_non_existing_user(page) { +async function expect_non_existing_user(page: Page): Promise { await page.waitForSelector("#non_existing_user", {visible: true}); const expected_message = "This user does not exist!"; assert.strictEqual( @@ -138,7 +144,7 @@ async function expect_non_existing_user(page) { ); } -async function expect_non_existing_users(page) { +async function expect_non_existing_users(page: Page): Promise { await page.waitForSelector("#non_existing_users", {visible: true}); const expected_message = "One or more of these users do not exist!"; assert.strictEqual( @@ -147,13 +153,13 @@ async function expect_non_existing_users(page) { ); } -async function search_non_existing_user(page, str, item) { +async function search_non_existing_user(page: Page, str: string, item: string): Promise { await common.select_item_via_typeahead(page, "#search_query", str, item); await expect_non_existing_user(page); await un_narrow(page); } -async function search_tests(page) { +async function search_tests(page: Page): Promise { await search_and_check( page, "Verona", @@ -217,7 +223,7 @@ async function search_tests(page) { ); } -async function expect_all_pm(page) { +async function expect_all_pm(page: Page): Promise { await page.waitForSelector("#zfilt", {visible: true}); await common.check_messages_sent(page, "zfilt", [ ["You and Cordelia Lear, King Hamlet", ["group pm a", "group pm b"]], @@ -232,7 +238,7 @@ async function expect_all_pm(page) { assert.strictEqual(await page.title(), "Private messages - Zulip Dev - Zulip"); } -async function test_narrow_by_clicking_the_left_sidebar(page) { +async function test_narrow_by_clicking_the_left_sidebar(page: Page): Promise { console.log("Narrowing with left sidebar"); await page.click((await get_stream_li(page, "Verona")) + " a"); @@ -247,11 +253,11 @@ async function test_narrow_by_clicking_the_left_sidebar(page) { await un_narrow(page); } -async function arrow(page, direction) { - await page.keyboard.press(`Arrow${direction}`); +async function arrow(page: Page, direction: "Up" | "Down"): Promise { + await page.keyboard.press(({Up: "ArrowUp", Down: "ArrowDown"} as const)[direction]); } -async function test_search_venice(page) { +async function test_search_venice(page: Page): Promise { await page.evaluate(() => { $(".stream-list-filter") .expectOne() @@ -279,7 +285,7 @@ async function test_search_venice(page) { await page.waitForSelector(".input-append.notdisplayed"); } -async function test_stream_search_filters_stream_list(page) { +async function test_stream_search_filters_stream_list(page: Page): Promise { console.log("Search streams using left side bar"); await page.waitForSelector(".input-append.notdisplayed"); // Stream filter box invisible initially @@ -342,22 +348,22 @@ async function test_stream_search_filters_stream_list(page) { await un_narrow(page); } -async function test_users_search(page) { +async function test_users_search(page: Page): Promise { console.log("Search users using right sidebar"); - async function assert_in_list(page, name) { + async function assert_in_list(page: Page, name: string): Promise { await page.waitForSelector(`#user_presences li [data-name="${CSS.escape(name)}"]`, { visible: true, }); } - async function assert_selected(page, name) { + async function assert_selected(page: Page, name: string): Promise { await page.waitForSelector( `#user_presences li.highlighted_user [data-name="${CSS.escape(name)}"]`, {visible: true}, ); } - async function assert_not_selected(page, name) { + async function assert_not_selected(page: Page, name: string): Promise { await page.waitForSelector( `#user_presences li.highlighted_user [data-name="${CSS.escape(name)}"]`, {hidden: true}, @@ -406,7 +412,7 @@ async function test_users_search(page) { await expect_cordelia_private_narrow(page); } -async function message_basic_tests(page) { +async function message_basic_tests(page: Page): Promise { await common.log_in(page); console.log("Sending messages"); diff --git a/frontend_tests/puppeteer_tests/03-compose.js b/frontend_tests/puppeteer_tests/03-compose.ts similarity index 80% rename from frontend_tests/puppeteer_tests/03-compose.js rename to frontend_tests/puppeteer_tests/03-compose.ts index 1fee3504cd..e2bb417d55 100644 --- a/frontend_tests/puppeteer_tests/03-compose.js +++ b/frontend_tests/puppeteer_tests/03-compose.ts @@ -1,10 +1,10 @@ -"use strict"; +import {strict as assert} from "assert"; -const {strict: assert} = require("assert"); +import type {Page} from "puppeteer"; -const common = require("../puppeteer_lib/common"); +import common from "../puppeteer_lib/common"; -async function check_compose_form_empty(page) { +async function check_compose_form_empty(page: Page): Promise { await common.check_form_contents(page, "#send_message_form", { stream_message_recipient_stream: "", stream_message_recipient_topic: "", @@ -12,20 +12,20 @@ async function check_compose_form_empty(page) { }); } -async function close_compose_box(page) { +async function close_compose_box(page: Page): Promise { await page.keyboard.press("Escape"); await page.waitForSelector("#compose-textarea", {hidden: true}); } -function get_message_xpath(text) { +function get_message_xpath(text: string): string { return `//p[text()='${text}']`; } -function get_last_element(array) { +function get_last_element(array: T[]): T { return array.slice(-1)[0]; } -async function test_send_messages(page) { +async function test_send_messages(page: Page): Promise { const initial_msgs_count = await page.evaluate(() => $("#zhome .message_row").length); await common.send_multiple_messages(page, [ @@ -39,26 +39,26 @@ async function test_send_messages(page) { ); } -async function test_stream_compose_keyboard_shortcut(page) { +async function test_stream_compose_keyboard_shortcut(page: Page): Promise { await page.keyboard.press("KeyC"); await page.waitForSelector("#stream-message", {visible: true}); await check_compose_form_empty(page); await close_compose_box(page); } -async function test_private_message_compose_shortcut(page) { +async function test_private_message_compose_shortcut(page: Page): Promise { await page.keyboard.press("KeyX"); await page.waitForSelector("#private_message_recipient", {visible: true}); await common.pm_recipient.expect(page, ""); await close_compose_box(page); } -async function test_keyboard_shortcuts(page) { +async function test_keyboard_shortcuts(page: Page): Promise { await test_stream_compose_keyboard_shortcut(page); await test_private_message_compose_shortcut(page); } -async function test_reply_by_click_prepopulates_stream_topic_names(page) { +async function test_reply_by_click_prepopulates_stream_topic_names(page: Page): Promise { const stream_message = get_last_element( await page.$x(get_message_xpath("Compose stream reply test")), ); @@ -72,7 +72,9 @@ async function test_reply_by_click_prepopulates_stream_topic_names(page) { await close_compose_box(page); } -async function test_reply_by_click_prepopulates_private_message_recipient(page) { +async function test_reply_by_click_prepopulates_private_message_recipient( + page: Page, +): Promise { const private_message = get_last_element( await page.$x(get_message_xpath("Compose private message reply test")), ); @@ -85,7 +87,7 @@ async function test_reply_by_click_prepopulates_private_message_recipient(page) await close_compose_box(page); } -async function test_reply_with_r_shortcut(page) { +async function test_reply_with_r_shortcut(page: Page): Promise { // The last message(private) in the narrow is currently selected as a result of previous tests. // Now we go up and open compose box with r key. await page.keyboard.press("KeyK"); @@ -97,7 +99,7 @@ async function test_reply_with_r_shortcut(page) { }); } -async function test_open_close_compose_box(page) { +async function test_open_close_compose_box(page: Page): Promise { await page.waitForSelector("#stream-message", {visible: true}); await close_compose_box(page); await page.waitForSelector("#stream-message", {hidden: true}); @@ -108,12 +110,12 @@ async function test_open_close_compose_box(page) { await page.waitForSelector("#private-message", {hidden: true}); } -async function test_narrow_to_private_messages_with_cordelia(page) { +async function test_narrow_to_private_messages_with_cordelia(page: Page): Promise { const you_and_cordelia_selector = '*[title="Narrow to your private messages with Cordelia Lear"]'; // For some unknown reason page.click() isn't working here. await page.evaluate( - (selector) => document.querySelector(selector).click(), + (selector: string) => document.querySelector(selector)!.click(), you_and_cordelia_selector, ); const cordelia_user_id = await common.get_user_id_from_name(page, "Cordelia Lear"); @@ -129,7 +131,7 @@ async function test_narrow_to_private_messages_with_cordelia(page) { await close_compose_box(page); } -async function test_send_multirecipient_pm_from_cordelia_pm_narrow(page) { +async function test_send_multirecipient_pm_from_cordelia_pm_narrow(page: Page): Promise { const recipients = ["cordelia@zulip.com", "othello@zulip.com"]; const multiple_recipients_pm = "A huddle to check spaces"; const pm_selector = `.messagebox:contains('${CSS.escape(multiple_recipients_pm)}')`; @@ -143,8 +145,8 @@ async function test_send_multirecipient_pm_from_cordelia_pm_narrow(page) { await page.keyboard.press("Escape"); await page.waitForSelector("#zhome .message_row", {visible: true}); - await page.waitForFunction((selector) => $(selector).length !== 0, {}, pm_selector); - await page.evaluate((selector) => { + await page.waitForFunction((selector: string) => $(selector).length !== 0, {}, pm_selector); + await page.evaluate((selector: string) => { $(selector).slice(-1)[0].click(); }, pm_selector); await page.waitForSelector("#compose-textarea", {visible: true}); @@ -158,7 +160,7 @@ async function test_send_multirecipient_pm_from_cordelia_pm_narrow(page) { const markdown_preview_button = "#markdown_preview"; const markdown_preview_hide_button = "#undo_markdown_preview"; -async function test_markdown_preview_buttons_visibility(page) { +async function test_markdown_preview_buttons_visibility(page: Page): Promise { await page.waitForSelector(markdown_preview_button, {visible: true}); await page.waitForSelector(markdown_preview_hide_button, {hidden: true}); @@ -173,22 +175,22 @@ async function test_markdown_preview_buttons_visibility(page) { await page.waitForSelector(markdown_preview_hide_button, {hidden: true}); } -async function test_markdown_preview_without_any_content(page) { +async function test_markdown_preview_without_any_content(page: Page): Promise { await page.click("#markdown_preview"); await page.waitForSelector("#undo_markdown_preview", {visible: true}); const markdown_preview_element = await page.$("#preview_content"); assert.equal( - await page.evaluate((element) => element.textContent, markdown_preview_element), + await page.evaluate((element: Element) => element.textContent, markdown_preview_element), "Nothing to preview", ); await page.click("#undo_markdown_preview"); } -async function test_markdown_rendering(page) { +async function test_markdown_rendering(page: Page): Promise { await page.waitForSelector("#markdown_preview", {visible: true}); let markdown_preview_element = await page.$("#preview_content"); assert.equal( - await page.evaluate((element) => element.textContent, markdown_preview_element), + await page.evaluate((element: Element) => element.textContent, markdown_preview_element), "", ); await common.fill_form(page, 'form[action^="/json/messages"]', { @@ -201,18 +203,18 @@ async function test_markdown_rendering(page) { await page.waitForFunction(() => $("#preview_content").html() !== ""); markdown_preview_element = await page.$("#preview_content"); assert.equal( - await page.evaluate((element) => element.innerHTML, markdown_preview_element), + await page.evaluate((element: Element) => element.innerHTML, markdown_preview_element), expected_markdown_html, ); } -async function test_markdown_preview(page) { +async function test_markdown_preview(page: Page): Promise { await test_markdown_preview_buttons_visibility(page); await test_markdown_preview_without_any_content(page); await test_markdown_rendering(page); } -async function compose_tests(page) { +async function compose_tests(page: Page): Promise { await common.log_in(page); await test_send_messages(page); await test_keyboard_shortcuts(page); diff --git a/frontend_tests/puppeteer_tests/04-subscriptions.js b/frontend_tests/puppeteer_tests/04-subscriptions.ts similarity index 77% rename from frontend_tests/puppeteer_tests/04-subscriptions.js rename to frontend_tests/puppeteer_tests/04-subscriptions.ts index 0a3730191b..520f844e51 100644 --- a/frontend_tests/puppeteer_tests/04-subscriptions.js +++ b/frontend_tests/puppeteer_tests/04-subscriptions.ts @@ -1,43 +1,44 @@ -"use strict"; +import {strict as assert} from "assert"; -const {strict: assert} = require("assert"); +import type {ElementHandle, Page} from "puppeteer"; -const common = require("../puppeteer_lib/common"); +import common from "../puppeteer_lib/common"; -async function user_checkbox(page, name) { +async function user_checkbox(page: Page, name: string): Promise { const user_id = await common.get_user_id_from_name(page, name); - return `#user-checkboxes [data-user-id="${CSS.escape(user_id)}"]`; + return `#user-checkboxes [data-user-id="${CSS.escape(user_id.toString())}"]`; } -async function user_span(page, name) { +async function user_span(page: Page, name: string): Promise { return (await user_checkbox(page, name)) + " input ~ span"; } -async function stream_checkbox(page, stream_name) { +async function stream_checkbox(page: Page, stream_name: string): Promise { const stream_id = await common.get_stream_id(page, stream_name); - return `#stream-checkboxes [data-stream-id="${CSS.escape(stream_id)}"]`; + return `#stream-checkboxes [data-stream-id="${CSS.escape(stream_id.toString())}"]`; } -async function stream_span(page, stream_name) { +async function stream_span(page: Page, stream_name: string): Promise { return (await stream_checkbox(page, stream_name)) + " input ~ span"; } -async function wait_for_checked(page, user_name, is_checked) { +async function wait_for_checked(page: Page, user_name: string, is_checked: boolean): Promise { const selector = await user_checkbox(page, user_name); await page.waitForFunction( - (selector, is_checked) => $(selector).find("input")[0].checked === is_checked, + (selector: string, is_checked: boolean) => + $(selector).find("input")[0].checked === is_checked, {}, selector, is_checked, ); } -async function stream_name_error(page) { +async function stream_name_error(page: Page): Promise { await page.waitForSelector("#stream_name_error", {visible: true}); return await common.get_text_from_selector(page, "#stream_name_error"); } -async function open_streams_modal(page) { +async function open_streams_modal(page: Page): Promise { const all_streams_selector = 'a[href="#streams/all"]'; await page.waitForSelector(all_streams_selector, {visible: true}); await page.click(all_streams_selector); @@ -46,16 +47,16 @@ async function open_streams_modal(page) { assert(page.url().includes("#streams/all")); } -async function test_subscription_button_verona_stream(page) { +async function test_subscription_button_verona_stream(page: Page): Promise { const button_selector = "[data-stream-name='Verona'] .sub_unsub_button"; const subscribed_selector = `${button_selector}.checked`; const unsubscribed_selector = `${button_selector}:not(.checked)`; - async function subscribed() { + async function subscribed(): Promise { return await page.waitForSelector(subscribed_selector, {visible: true}); } - async function unsubscribed() { + async function unsubscribed(): Promise { return await page.waitForSelector(unsubscribed_selector, {visible: true}); } @@ -66,57 +67,69 @@ async function test_subscription_button_verona_stream(page) { // We assume Verona is already subscribed, so the first line here // should happen immediately. button = await subscribed(); - button.click(); + button!.click(); button = await unsubscribed(); - button.click(); + button!.click(); button = await subscribed(); - button.click(); + button!.click(); button = await unsubscribed(); - button.click(); + button!.click(); button = await subscribed(); } -async function click_create_new_stream(page, cordelia_checkbox, othello_checkbox) { +async function click_create_new_stream( + page: Page, + cordelia_checkbox: string, + othello_checkbox: string, +): Promise { await page.click("#add_new_subscription .create_stream_button"); await page.waitForSelector(cordelia_checkbox, {visible: true}); await page.waitForSelector(othello_checkbox, {visible: true}); } -async function open_copy_from_stream_dropdown(page, scotland_checkbox, rome_checkbox) { +async function open_copy_from_stream_dropdown( + page: Page, + scotland_checkbox: string, + rome_checkbox: string, +): Promise { await page.click("#copy-from-stream-expand-collapse .control-label"); await page.waitForSelector(scotland_checkbox, {visible: true}); await page.waitForSelector(rome_checkbox, {visible: true}); } -async function test_check_all_only_affects_visible_users(page) { +async function test_check_all_only_affects_visible_users(page: Page): Promise { await page.click(".subs_set_all_users"); await wait_for_checked(page, "cordelia", false); await wait_for_checked(page, "othello", true); } -async function test_uncheck_all(page) { +async function test_uncheck_all(page: Page): Promise { await page.click(".subs_unset_all_users"); await wait_for_checked(page, "othello", false); } -async function clear_ot_filter_with_backspace(page) { +async function clear_ot_filter_with_backspace(page: Page): Promise { await page.click(".add-user-list-filter"); await page.keyboard.press("Backspace"); await page.keyboard.press("Backspace"); } -async function verify_filtered_users_are_visible_again(page, cordelia_checkbox, othello_checkbox) { +async function verify_filtered_users_are_visible_again( + page: Page, + cordelia_checkbox: string, + othello_checkbox: string, +): Promise { await page.waitForSelector(cordelia_checkbox, {visible: true}); await page.waitForSelector(othello_checkbox, {visible: true}); } async function test_user_filter_ui( - page, - cordelia_checkbox, - othello_checkbox, - scotland_checkbox, - rome_checkbox, -) { + page: Page, + cordelia_checkbox: string, + othello_checkbox: string, + scotland_checkbox: string, + rome_checkbox: string, +): Promise { await page.waitForSelector("form#stream_creation_form", {visible: true}); await common.fill_form(page, "form#stream_creation_form", {user_list_filter: "ot"}); @@ -135,7 +148,7 @@ async function test_user_filter_ui( await verify_filtered_users_are_visible_again(page, cordelia_checkbox, othello_checkbox); } -async function create_stream(page) { +async function create_stream(page: Page): Promise { await page.waitForXPath('//*[text()="Create stream"]', {visible: true}); await common.fill_form(page, "form#stream_creation_form", { stream_name: "Puppeteer", @@ -162,13 +175,13 @@ async function create_stream(page) { // Assert subscriber count becomes 5(scotland(+4), cordelia(+1), othello(-1), Desdemona(+1)). await page.waitForFunction( - (subscriber_count_selector) => $(subscriber_count_selector).text().trim() === "5", + (subscriber_count_selector: string) => $(subscriber_count_selector).text().trim() === "5", {}, subscriber_count_selector, ); } -async function test_streams_with_empty_names_cannot_be_created(page) { +async function test_streams_with_empty_names_cannot_be_created(page: Page): Promise { await page.click("#add_new_subscription .create_stream_button"); await page.waitForSelector("form#stream_creation_form", {visible: true}); await common.fill_form(page, "form#stream_creation_form", {stream_name: " "}); @@ -176,7 +189,7 @@ async function test_streams_with_empty_names_cannot_be_created(page) { assert.strictEqual(await stream_name_error(page), "A stream needs to have a name"); } -async function test_streams_with_duplicate_names_cannot_be_created(page) { +async function test_streams_with_duplicate_names_cannot_be_created(page: Page): Promise { await common.fill_form(page, "form#stream_creation_form", {stream_name: "Puppeteer"}); await page.click("form#stream_creation_form button.button.sea-green"); assert.strictEqual(await stream_name_error(page), "A stream with this name already exists"); @@ -185,7 +198,7 @@ async function test_streams_with_duplicate_names_cannot_be_created(page) { await page.click(cancel_button_selector); } -async function test_stream_creation(page) { +async function test_stream_creation(page: Page): Promise { const cordelia_checkbox = await user_checkbox(page, "cordelia"); const othello_checkbox = await user_checkbox(page, "othello"); const scotland_checkbox = await stream_checkbox(page, "Scotland"); @@ -205,7 +218,7 @@ async function test_stream_creation(page) { await test_streams_with_duplicate_names_cannot_be_created(page); } -async function test_streams_search_feature(page) { +async function test_streams_search_feature(page: Page): Promise { assert.strictEqual(await common.get_text_from_selector(page, "#search_stream_name"), ""); const hidden_streams_selector = ".stream-row.notdisplayed .stream-name"; assert.strictEqual( @@ -235,7 +248,7 @@ async function test_streams_search_feature(page) { ); } -async function subscriptions_tests(page) { +async function subscriptions_tests(page: Page): Promise { await common.log_in(page); await open_streams_modal(page); await test_subscription_button_verona_stream(page); diff --git a/frontend_tests/puppeteer_tests/05-stars.js b/frontend_tests/puppeteer_tests/05-stars.ts similarity index 79% rename from frontend_tests/puppeteer_tests/05-stars.js rename to frontend_tests/puppeteer_tests/05-stars.ts index 3ff498e6f5..8a53e730e9 100644 --- a/frontend_tests/puppeteer_tests/05-stars.js +++ b/frontend_tests/puppeteer_tests/05-stars.ts @@ -1,17 +1,17 @@ -"use strict"; +import {strict as assert} from "assert"; -const {strict: assert} = require("assert"); +import type {Page} from "puppeteer"; -const common = require("../puppeteer_lib/common"); +import common from "../puppeteer_lib/common"; const message = "test star"; -async function stars_count(page) { +async function stars_count(page: Page): Promise { return await page.evaluate(() => $("#zhome .fa-star:not(.empty-star)").length); } -async function toggle_test_star_message(page) { - await page.evaluate((message) => { +async function toggle_test_star_message(page: Page): Promise { + await page.evaluate((message: string) => { const msg = $(`.message_content:contains("${CSS.escape(message)}"):visible`).last(); if (msg.length !== 1) { throw new Error("cannot find test star message"); @@ -26,7 +26,7 @@ async function toggle_test_star_message(page) { }, message); } -async function test_narrow_to_starred_messages(page) { +async function test_narrow_to_starred_messages(page: Page): Promise { await page.click('a[href^="#narrow/is/starred"]'); await common.check_messages_sent(page, "zfilt", [["Verona > stars", [message]]]); @@ -35,7 +35,7 @@ async function test_narrow_to_starred_messages(page) { await page.waitForSelector("#zhome .message_row", {visible: true}); } -async function stars_test(page) { +async function stars_test(page: Page): Promise { await common.log_in(page); await common.send_message(page, "stream", { stream: "Verona", diff --git a/frontend_tests/puppeteer_tests/06-edit.js b/frontend_tests/puppeteer_tests/06-edit.ts similarity index 82% rename from frontend_tests/puppeteer_tests/06-edit.js rename to frontend_tests/puppeteer_tests/06-edit.ts index 6664f0491b..78b057f35e 100644 --- a/frontend_tests/puppeteer_tests/06-edit.js +++ b/frontend_tests/puppeteer_tests/06-edit.ts @@ -1,8 +1,8 @@ -"use strict"; +import type {Page} from "puppeteer"; -const common = require("../puppeteer_lib/common"); +import common from "../puppeteer_lib/common"; -async function trigger_edit_last_message(page) { +async function trigger_edit_last_message(page: Page): Promise { await page.evaluate(() => { const msg = $("#zhome .message_row").last(); msg.find(".info").trigger("click"); @@ -11,7 +11,7 @@ async function trigger_edit_last_message(page) { await page.waitForSelector(".message_edit_content", {visible: true}); } -async function edit_stream_message(page, topic, content) { +async function edit_stream_message(page: Page, topic: string, content: string): Promise { await trigger_edit_last_message(page); await page.evaluate(() => $(".message_edit_topic").val("")); @@ -24,7 +24,7 @@ async function edit_stream_message(page, topic, content) { await common.wait_for_fully_processed_message(page, content); } -async function test_stream_message_edit(page) { +async function test_stream_message_edit(page: Page): Promise { await common.send_message(page, "stream", { stream: "Verona", topic: "edits", @@ -36,7 +36,7 @@ async function test_stream_message_edit(page) { await common.check_messages_sent(page, "zhome", [["Verona > edited", ["test edited"]]]); } -async function test_edit_message_with_slash_me(page) { +async function test_edit_message_with_slash_me(page: Page): Promise { await common.send_message(page, "stream", { stream: "Verona", topic: "edits", @@ -59,7 +59,7 @@ async function test_edit_message_with_slash_me(page) { ); } -async function test_edit_private_message(page) { +async function test_edit_private_message(page: Page): Promise { await common.send_message(page, "private", { recipient: "cordelia@zulip.com", content: "test editing pm", @@ -76,7 +76,7 @@ async function test_edit_private_message(page) { ]); } -async function edit_tests(page) { +async function edit_tests(page: Page): Promise { await common.log_in(page); await test_stream_message_edit(page); diff --git a/frontend_tests/puppeteer_tests/07-navigation.js b/frontend_tests/puppeteer_tests/07-navigation.ts similarity index 84% rename from frontend_tests/puppeteer_tests/07-navigation.js rename to frontend_tests/puppeteer_tests/07-navigation.ts index a3dd2d5b42..d056ffb953 100644 --- a/frontend_tests/puppeteer_tests/07-navigation.js +++ b/frontend_tests/puppeteer_tests/07-navigation.ts @@ -1,28 +1,28 @@ -"use strict"; +import {strict as assert} from "assert"; -const {strict: assert} = require("assert"); +import type {Page} from "puppeteer"; -const common = require("../puppeteer_lib/common"); +import common from "../puppeteer_lib/common"; -async function wait_for_tab(page, tab) { +async function wait_for_tab(page: Page, tab: string): Promise { const tab_slector = `#${CSS.escape(tab)}.tab-pane.active`; await page.waitForSelector(tab_slector, {visible: true}); } -async function navigate_to(page, click_target, tab) { +async function navigate_to(page: Page, click_target: string, tab: string): Promise { console.log("Visiting #" + click_target); await page.click(`a[href='#${CSS.escape(click_target)}']`); await wait_for_tab(page, tab); } -async function open_menu(page) { +async function open_menu(page: Page): Promise { const menu_selector = "#settings-dropdown"; await page.waitForSelector(menu_selector, {visible: true}); await page.click(menu_selector); } -async function navigate_to_settings(page) { +async function navigate_to_settings(page: Page): Promise { console.log("Navigating to settings"); await open_menu(page); @@ -36,7 +36,7 @@ async function navigate_to_settings(page) { await page.click("#settings_page .content-wrapper .exit"); } -async function navigate_to_subscriptions(page) { +async function navigate_to_subscriptions(page: Page): Promise { console.log("Navigate to subscriptions"); await open_menu(page); @@ -51,7 +51,7 @@ async function navigate_to_subscriptions(page) { await page.click("#subscription_overlay .exit"); } -async function test_reload_hash(page) { +async function test_reload_hash(page: Page): Promise { const initial_page_load_time = await page.evaluate(() => page_params.page_load_time); console.log("initial load time: " + initial_page_load_time); @@ -70,7 +70,7 @@ async function test_reload_hash(page) { assert.strictEqual(hash, initial_hash, "Hash not preserved."); } -async function navigation_tests(page) { +async function navigation_tests(page: Page): Promise { await common.log_in(page); await navigate_to_settings(page); diff --git a/frontend_tests/puppeteer_tests/08-admin.js b/frontend_tests/puppeteer_tests/08-admin.ts similarity index 81% rename from frontend_tests/puppeteer_tests/08-admin.js rename to frontend_tests/puppeteer_tests/08-admin.ts index 06f68af318..82967c4f96 100644 --- a/frontend_tests/puppeteer_tests/08-admin.js +++ b/frontend_tests/puppeteer_tests/08-admin.ts @@ -1,10 +1,10 @@ -"use strict"; +import {strict as assert} from "assert"; -const {strict: assert} = require("assert"); +import type {Page} from "puppeteer"; -const common = require("../puppeteer_lib/common"); +import common from "../puppeteer_lib/common"; -async function submit_notifications_stream_settings(page) { +async function submit_notifications_stream_settings(page: Page): Promise { await page.waitForSelector('#org-submit-notifications[data-status="unsaved"]', {visible: true}); const save_button = "#org-submit-notifications"; @@ -25,7 +25,7 @@ async function submit_notifications_stream_settings(page) { await page.waitForSelector("#org-submit-notifications", {hidden: true}); } -async function test_change_new_stream_notifications_setting(page) { +async function test_change_new_stream_notifications_setting(page: Page): Promise { await page.click("#realm_notifications_stream_id_widget button.dropdown-toggle"); await page.waitForSelector("#realm_notifications_stream_id_widget ul.dropdown-menu", { visible: true, @@ -41,7 +41,7 @@ async function test_change_new_stream_notifications_setting(page) { await common.wait_for_text(page, verona_in_dropdown, "Verona"); await page.waitForSelector(verona_in_dropdown, {visible: true}); - await page.evaluate((selector) => $(selector).trigger("click"), verona_in_dropdown); + await page.evaluate((selector: string) => $(selector).trigger("click"), verona_in_dropdown); await submit_notifications_stream_settings(page); @@ -52,7 +52,7 @@ async function test_change_new_stream_notifications_setting(page) { await submit_notifications_stream_settings(page); } -async function test_change_signup_notifications_stream(page) { +async function test_change_signup_notifications_stream(page: Page): Promise { console.log('Changing signup notifications stream to Verona by filtering with "verona"'); await page.click("#id_realm_signup_notifications_stream_id > button.dropdown-toggle"); @@ -79,7 +79,7 @@ async function test_change_signup_notifications_stream(page) { await submit_notifications_stream_settings(page); } -async function test_permissions_change_save_worked(page) { +async function test_permissions_change_save_worked(page: Page): Promise { const saved_status = '#org-submit-stream-permissions[data-status="saved"]'; await page.waitForSelector(saved_status, { visible: true, @@ -87,7 +87,7 @@ async function test_permissions_change_save_worked(page) { await page.waitForSelector(saved_status, {hidden: true}); } -async function submit_stream_permissions_change(page) { +async function submit_stream_permissions_change(page: Page): Promise { const save_button = "#org-submit-stream-permissions"; await page.waitForSelector(save_button, {visible: true}); assert.strictEqual( @@ -100,49 +100,49 @@ async function submit_stream_permissions_change(page) { await test_permissions_change_save_worked(page); } -async function test_set_create_streams_to_admins_only(page) { +async function test_set_create_streams_to_admins_only(page: Page): Promise { console.log("Test setting create streams policy to 'admins only'."); await page.waitForSelector("#id_realm_create_stream_policy", {visible: true}); await page.evaluate(() => $("#id_realm_create_stream_policy").val(2).trigger("change")); await submit_stream_permissions_change(page); } -async function test_set_create_streams_to_members_and_admins(page) { +async function test_set_create_streams_to_members_and_admins(page: Page): Promise { console.log("Test setting create streams policy to 'members and admins'."); await page.waitForSelector("#id_realm_create_stream_policy", {visible: true}); await page.evaluate(() => $("#id_realm_create_stream_policy").val(1).trigger("change")); await submit_stream_permissions_change(page); } -async function test_set_create_streams_policy_to_full_members(page) { +async function test_set_create_streams_policy_to_full_members(page: Page): Promise { console.log("Test setting create streams policy to 'full members'."); await page.waitForSelector("#id_realm_create_stream_policy", {visible: true}); await page.evaluate(() => $("#id_realm_create_stream_policy").val(3).trigger("change")); await submit_stream_permissions_change(page); } -async function test_set_invite_to_streams_policy_to_admins_only(page) { +async function test_set_invite_to_streams_policy_to_admins_only(page: Page): Promise { console.log("Test setting invite to streams policy to 'admins only'."); await page.waitForSelector("#id_realm_invite_to_stream_policy", {visible: true}); await page.evaluate(() => $("#id_realm_invite_to_stream_policy").val(2).trigger("change")); await submit_stream_permissions_change(page); } -async function test_set_invite_to_streams_policy_to_members_and_admins(page) { +async function test_set_invite_to_streams_policy_to_members_and_admins(page: Page): Promise { console.log("Test setting invite to streams policy to 'members and admins'."); await page.waitForSelector("#id_realm_invite_to_stream_policy", {visible: true}); await page.evaluate(() => $("#id_realm_invite_to_stream_policy").val(1).trigger("change")); await submit_stream_permissions_change(page); } -async function test_set_invite_to_streams_policy_to_full_members(page) { +async function test_set_invite_to_streams_policy_to_full_members(page: Page): Promise { console.log("Test setting invite to streams policy to 'full members'."); await page.waitForSelector("#id_realm_invite_to_stream_policy", {visible: true}); await page.evaluate(() => $("#id_realm_invite_to_stream_policy").val(3).trigger("change")); await submit_stream_permissions_change(page); } -async function test_save_joining_organization_change_worked(page) { +async function test_save_joining_organization_change_worked(page: Page): Promise { const saved_status = '#org-submit-org-join[data-status="saved"]'; await page.waitForSelector(saved_status, { visible: true, @@ -150,7 +150,7 @@ async function test_save_joining_organization_change_worked(page) { await page.waitForSelector(saved_status, {hidden: true}); } -async function submit_joining_organization_change(page) { +async function submit_joining_organization_change(page: Page): Promise { const save_button = "#org-submit-org-join"; await page.waitForSelector(save_button, {visible: true}); assert.strictEqual( @@ -164,24 +164,28 @@ async function submit_joining_organization_change(page) { await test_save_joining_organization_change_worked(page); } -async function test_set_new_user_threshold_to_three_days(page) { +async function test_set_new_user_threshold_to_three_days(page: Page): Promise { console.log("Test setting new user threshold to three days."); await page.waitForSelector("#id_realm_waiting_period_setting", {visible: true}); - await page.evaluate(() => $("#id_realm_waiting_period_setting").val("three_days").trigger("change")); + await page.evaluate(() => + $("#id_realm_waiting_period_setting").val("three_days").trigger("change"), + ); await submit_joining_organization_change(page); } -async function test_set_new_user_threshold_to_N_days(page) { +async function test_set_new_user_threshold_to_N_days(page: Page): Promise { console.log("Test setting new user threshold to three days."); await page.waitForSelector("#id_realm_waiting_period_setting", {visible: true}); - await page.evaluate(() => $("#id_realm_waiting_period_setting").val("custom_days").trigger("change")); + await page.evaluate(() => + $("#id_realm_waiting_period_setting").val("custom_days").trigger("change"), + ); const N = 10; - await page.evaluate((N) => $("#id_realm_waiting_period_threshold").val(N), N); + await page.evaluate((N: number) => $("#id_realm_waiting_period_threshold").val(N), N); await submit_joining_organization_change(page); } -async function test_organization_permissions(page) { +async function test_organization_permissions(page: Page): Promise { await page.click("li[data-section='organization-permissions']"); await test_set_create_streams_to_admins_only(page); @@ -196,11 +200,11 @@ async function test_organization_permissions(page) { await test_set_new_user_threshold_to_N_days(page); } -async function test_add_emoji(page) { +async function test_add_emoji(page: Page): Promise { await common.fill_form(page, "form.admin-emoji-form", {name: "zulip logo"}); const emoji_upload_handle = await page.$("#emoji_file_input"); - await emoji_upload_handle.uploadFile("static/images/logo/zulip-icon-128x128.png"); + await emoji_upload_handle!.uploadFile("static/images/logo/zulip-icon-128x128.png"); await page.click("#admin_emoji_submit"); const emoji_status = "div#admin-emoji-status"; @@ -219,14 +223,14 @@ async function test_add_emoji(page) { await page.waitForSelector("tr#emoji_zulip_logo img", {visible: true}); } -async function test_delete_emoji(page) { +async function test_delete_emoji(page: Page): Promise { await page.click("tr#emoji_zulip_logo button.delete"); // assert the emoji is deleted. await page.waitForFunction(() => $("tr#emoji_zulip_logo").length === 0); } -async function test_custom_realm_emoji(page) { +async function test_custom_realm_emoji(page: Page): Promise { await page.click("li[data-section='emoji-settings']"); await page.waitForSelector(".admin-emoji-form", {visible: true}); @@ -234,8 +238,8 @@ async function test_custom_realm_emoji(page) { await test_delete_emoji(page); } -async function get_suggestions(page, str) { - await page.evaluate((str) => { +async function get_suggestions(page: Page, str: string): Promise { + await page.evaluate((str: string) => { $(".create_default_stream") .trigger("focus") .val(str) @@ -243,8 +247,8 @@ async function get_suggestions(page, str) { }, str); } -async function select_from_suggestions(page, item) { - await page.evaluate((item) => { +async function select_from_suggestions(page: Page, item: string): Promise { + await page.evaluate((item: string) => { const tah = $(".create_default_stream").data().typeahead; tah.mouseenter({ currentTarget: $(`.typeahead:visible li:contains("${CSS.escape(item)}")`)[0], @@ -253,7 +257,11 @@ async function select_from_suggestions(page, item) { }, item); } -async function test_add_default_stream(page, stream_name, row) { +async function test_add_default_stream( + page: Page, + stream_name: string, + row: string, +): Promise { // It matches with all the stream names which has 'O' as a substring (Rome, Scotland, Verona // etc). 'O' is used to make sure that it works even if there are multiple suggestions. // Uppercase 'O' is used instead of the lowercase version to make sure that the suggestions @@ -265,28 +273,28 @@ async function test_add_default_stream(page, stream_name, row) { await page.waitForSelector(row, {visible: true}); } -async function test_remove_default_stream(page, row) { +async function test_remove_default_stream(page: Page, row: string): Promise { await page.click(row + " button.remove-default-stream"); // assert row doesn't exist. - await page.waitForFunction((row) => $(row).length === 0, {}, row); + await page.waitForFunction((row: string) => $(row).length === 0, {}, row); } -async function test_default_streams(page) { +async function test_default_streams(page: Page): Promise { await page.click("li[data-section='default-streams-list']"); await page.waitForSelector(".create_default_stream", {visible: true}); const stream_name = "Scotland"; const stream_id = await common.get_stream_id(page, stream_name); - const row = `.default_stream_row[data-stream-id='${CSS.escape(stream_id)}']`; + const row = `.default_stream_row[data-stream-id='${CSS.escape(stream_id.toString())}']`; await test_add_default_stream(page, stream_name, row); await test_remove_default_stream(page, row); } -async function test_upload_realm_icon_image(page) { +async function test_upload_realm_icon_image(page: Page): Promise { const upload_handle = await page.$("#realm-icon-upload-widget .image_file_input"); - await upload_handle.uploadFile("static/images/logo/zulip-icon-128x128.png"); + await upload_handle!.uploadFile("static/images/logo/zulip-icon-128x128.png"); await page.waitForSelector("#realm-icon-upload-widget .upload-spinner-background", { visible: true, @@ -300,14 +308,14 @@ async function test_upload_realm_icon_image(page) { ); } -async function delete_realm_icon(page) { +async function delete_realm_icon(page: Page): Promise { await page.click("li[data-section='organization-profile']"); await page.click("#realm-icon-upload-widget .image-delete-button"); await page.waitForSelector("#realm-icon-upload-widget .image-delete-button", {visible: false}); } -async function test_organization_profile(page) { +async function test_organization_profile(page: Page): Promise { await page.click("li[data-section='organization-profile']"); const gravatar_selctor = '#realm-icon-upload-widget .image-block[src^="https://secure.gravatar.com/avatar/"]'; @@ -322,7 +330,7 @@ async function test_organization_profile(page) { await page.waitForSelector(gravatar_selctor, {visible: true}); } -async function submit_default_user_settings(page) { +async function submit_default_user_settings(page: Page): Promise { assert.strictEqual( await common.get_text_from_selector(page, "#org-submit-user-defaults"), "Save changes", @@ -332,7 +340,7 @@ async function submit_default_user_settings(page) { await page.waitForSelector(saved_status, {visible: false}); } -async function test_change_organization_default_language(page) { +async function test_change_organization_default_language(page: Page): Promise { console.log("Changing realm default language"); await page.click("li[data-section='organization-settings']"); await page.waitForSelector("#id_realm_default_language", {visible: true}); @@ -341,7 +349,7 @@ async function test_change_organization_default_language(page) { await submit_default_user_settings(page); } -async function test_authentication_methods(page) { +async function test_authentication_methods(page: Page): Promise { await page.click("li[data-section='auth-methods']"); await page.waitForSelector(".method_row[data-method='Google'] input[type='checkbox'] + span", { visible: true, @@ -364,11 +372,13 @@ async function test_authentication_methods(page) { visible: true, }); await page.waitForFunction( - () => !$(".method_row[data-method='Google'] input[type='checkbox']")[0].checked, + () => + !($(".method_row[data-method='Google'] input[type='checkbox']")[0] as HTMLInputElement) + .checked, ); } -async function admin_test(page) { +async function admin_test(page: Page): Promise { await common.log_in(page); await common.manage_organization(page); diff --git a/frontend_tests/puppeteer_tests/09-mention.js b/frontend_tests/puppeteer_tests/09-mention.ts similarity index 89% rename from frontend_tests/puppeteer_tests/09-mention.js rename to frontend_tests/puppeteer_tests/09-mention.ts index 21f638f3af..8ccb43c0e2 100644 --- a/frontend_tests/puppeteer_tests/09-mention.js +++ b/frontend_tests/puppeteer_tests/09-mention.ts @@ -1,10 +1,10 @@ -"use strict"; +import {strict as assert} from "assert"; -const {strict: assert} = require("assert"); +import type {Page} from "puppeteer"; -const common = require("../puppeteer_lib/common"); +import common from "../puppeteer_lib/common"; -async function test_mention(page) { +async function test_mention(page: Page): Promise { await common.log_in(page); await page.keyboard.press("KeyC"); await page.waitForSelector("#compose", {visible: true}); diff --git a/frontend_tests/puppeteer_tests/10-custom-profile.js b/frontend_tests/puppeteer_tests/10-custom-profile.ts similarity index 85% rename from frontend_tests/puppeteer_tests/10-custom-profile.js rename to frontend_tests/puppeteer_tests/10-custom-profile.ts index 4299061e11..cb0148541e 100644 --- a/frontend_tests/puppeteer_tests/10-custom-profile.js +++ b/frontend_tests/puppeteer_tests/10-custom-profile.ts @@ -1,14 +1,14 @@ -"use strict"; +import {strict as assert} from "assert"; -const {strict: assert} = require("assert"); +import type {Page} from "puppeteer"; -const common = require("../puppeteer_lib/common"); +import common from "../puppeteer_lib/common"; // These will be the row and edit form of the the custom profile we add. const profile_field_row = "#admin_profile_fields_table tr:nth-last-child(2)"; const profile_field_form = "#admin_profile_fields_table tr:nth-last-child(1)"; -async function test_add_new_profile_field(page) { +async function test_add_new_profile_field(page: Page): Promise { await page.waitForSelector(".admin-profile-field-form", {visible: true}); await common.fill_form(page, "form.admin-profile-field-form", { name: "Teams", @@ -28,7 +28,7 @@ async function test_add_new_profile_field(page) { ); } -async function test_edit_profile_field(page) { +async function test_edit_profile_field(page: Page): Promise { await page.click(`${profile_field_row} button.open-edit-form`); await page.waitForSelector(`${profile_field_form} form.name-setting`, {visible: true}); await common.fill_form(page, `${profile_field_form} form.name-setting`, { @@ -44,7 +44,7 @@ async function test_edit_profile_field(page) { ); } -async function test_delete_custom_profile_field(page) { +async function test_delete_custom_profile_field(page: Page): Promise { await page.click(`${profile_field_row} button.delete`); await page.waitForSelector("#admin-profile-field-status img", {visible: true}); assert.strictEqual( @@ -53,7 +53,7 @@ async function test_delete_custom_profile_field(page) { ); } -async function test_custom_profile(page) { +async function test_custom_profile(page: Page): Promise { await common.log_in(page); await common.manage_organization(page); diff --git a/frontend_tests/puppeteer_tests/11-user-deactivation.js b/frontend_tests/puppeteer_tests/11-user-deactivation.ts similarity index 84% rename from frontend_tests/puppeteer_tests/11-user-deactivation.js rename to frontend_tests/puppeteer_tests/11-user-deactivation.ts index 3504a064a5..12ce853519 100644 --- a/frontend_tests/puppeteer_tests/11-user-deactivation.js +++ b/frontend_tests/puppeteer_tests/11-user-deactivation.ts @@ -1,10 +1,10 @@ -"use strict"; +import {strict as assert} from "assert"; -const {strict: assert} = require("assert"); +import type {Page} from "puppeteer"; -const common = require("../puppeteer_lib/common"); +import common from "../puppeteer_lib/common"; -async function navigate_to_user_list(page) { +async function navigate_to_user_list(page: Page): Promise { const menu_selector = "#settings-dropdown"; await page.waitForSelector(menu_selector, {visible: true}); await page.click(menu_selector); @@ -13,12 +13,12 @@ async function navigate_to_user_list(page) { await page.click("li[data-section='user-list-admin']"); } -async function user_row(page, name) { +async function user_row(page: Page, name: string): Promise { const user_id = await common.get_user_id_from_name(page, name); - return `.user_row[data-user-id="${CSS.escape(user_id)}"]`; + return `.user_row[data-user-id="${CSS.escape(user_id.toString())}"]`; } -async function test_deactivate_user(page) { +async function test_deactivate_user(page: Page): Promise { const cordelia_user_row = await user_row(page, "cordelia"); await page.waitForSelector(cordelia_user_row, {visible: true}); await page.waitForSelector(cordelia_user_row + " .fa-user-times"); @@ -39,7 +39,7 @@ async function test_deactivate_user(page) { await page.waitForSelector("#user-field-status", {hidden: true}); } -async function test_reactivate_user(page) { +async function test_reactivate_user(page: Page): Promise { let cordelia_user_row = await user_row(page, "cordelia"); await page.waitForSelector(cordelia_user_row + ".deactivated_user"); await page.waitForSelector(cordelia_user_row + " .fa-user-plus"); @@ -51,7 +51,7 @@ async function test_reactivate_user(page) { await page.waitForSelector("#user-field-status", {hidden: true}); } -async function test_deactivated_users_section(page) { +async function test_deactivated_users_section(page: Page): Promise { const cordelia_user_row = await user_row(page, "cordelia"); await test_deactivate_user(page); @@ -72,7 +72,7 @@ async function test_deactivated_users_section(page) { ); } -async function test_bot_deactivation_and_reactivation(page) { +async function test_bot_deactivation_and_reactivation(page: Page): Promise { await page.click("li[data-section='bot-list-admin']"); const default_bot_user_row = await user_row(page, "Zulip Default Bot"); @@ -87,7 +87,7 @@ async function test_bot_deactivation_and_reactivation(page) { await page.waitForSelector(default_bot_user_row + " .fa-user-times"); } -async function user_deactivation_test(page) { +async function user_deactivation_test(page: Page): Promise { await common.log_in(page); await navigate_to_user_list(page); await test_deactivate_user(page); diff --git a/frontend_tests/puppeteer_tests/12-drafts.js b/frontend_tests/puppeteer_tests/12-drafts.ts similarity index 87% rename from frontend_tests/puppeteer_tests/12-drafts.js rename to frontend_tests/puppeteer_tests/12-drafts.ts index f634f27cf7..1e151185a4 100644 --- a/frontend_tests/puppeteer_tests/12-drafts.js +++ b/frontend_tests/puppeteer_tests/12-drafts.ts @@ -1,22 +1,22 @@ -"use strict"; +import {strict as assert} from "assert"; -const {strict: assert} = require("assert"); +import type {Page} from "puppeteer"; -const common = require("../puppeteer_lib/common"); +import common from "../puppeteer_lib/common"; -async function wait_for_drafts_to_dissapear(page) { +async function wait_for_drafts_to_dissapear(page: Page): Promise { await page.waitForFunction( () => $("#draft_overlay").length === 0 || $("#draft_overlay").css("opacity") === "0", ); } -async function wait_for_drafts_to_appear(page) { +async function wait_for_drafts_to_appear(page: Page): Promise { await page.waitForFunction( () => $("#draft_overlay").length === 1 && $("#draft_overlay").css("opacity") === "1", ); } -async function get_drafts_count(page) { +async function get_drafts_count(page: Page): Promise { return await page.$$eval(".draft-row", (drafts) => drafts.length); } @@ -24,7 +24,7 @@ const drafts_button = ".compose_drafts_button"; const drafts_overlay = "#draft_overlay"; const drafts_button_in_compose = "#below-compose-content .drafts-link"; -async function test_empty_drafts(page) { +async function test_empty_drafts(page: Page): Promise { await page.waitForSelector(drafts_button, {visible: true}); await page.click(drafts_button); @@ -36,7 +36,7 @@ async function test_empty_drafts(page) { await wait_for_drafts_to_dissapear(page); } -async function create_stream_message_draft(page) { +async function create_stream_message_draft(page: Page): Promise { console.log("Creating Stream Message Draft"); await page.keyboard.press("KeyC"); await page.waitForSelector("#stream-message", {visible: true}); @@ -48,7 +48,7 @@ async function create_stream_message_draft(page) { await page.click("#compose_close"); } -async function create_private_message_draft(page) { +async function create_private_message_draft(page: Page): Promise { console.log("Creating private message draft"); await page.keyboard.press("KeyX"); await page.waitForSelector("#private_message_recipient", {visible: true}); @@ -58,7 +58,7 @@ async function create_private_message_draft(page) { await page.click("#compose_close"); } -async function open_compose_markdown_preview(page) { +async function open_compose_markdown_preview(page: Page): Promise { const new_topic_button = "#left_bar_compose_stream_button_big"; await page.waitForSelector(new_topic_button, {visible: true}); await page.click(new_topic_button); @@ -68,14 +68,14 @@ async function open_compose_markdown_preview(page) { await page.click(markdown_preview_button); } -async function open_drafts_through_compose(page) { +async function open_drafts_through_compose(page: Page): Promise { await open_compose_markdown_preview(page); await page.waitForSelector(drafts_button_in_compose, {visible: true}); await page.click(drafts_button_in_compose); await wait_for_drafts_to_appear(page); } -async function test_previously_created_drafts_rendered(page) { +async function test_previously_created_drafts_rendered(page: Page): Promise { const drafts_count = await get_drafts_count(page); assert.strictEqual(drafts_count, 2, "Drafts improperly loaded."); assert.strictEqual( @@ -109,7 +109,7 @@ async function test_previously_created_drafts_rendered(page) { ); } -async function test_restore_message_draft(page) { +async function test_restore_message_draft(page: Page): Promise { console.log("Restoring Stream Message Draft"); await page.click("#drafts_table .message_row:not(.private-message) .restore-draft"); await wait_for_drafts_to_dissapear(page); @@ -127,7 +127,7 @@ async function test_restore_message_draft(page) { ); } -async function edit_stream_message_draft(page) { +async function edit_stream_message_draft(page: Page): Promise { await common.fill_form(page, "form#send_message_form", { stream_message_recipient_stream: "all", stream_message_recipient_topic: "tests", @@ -136,7 +136,7 @@ async function edit_stream_message_draft(page) { await page.click("#compose_close"); } -async function test_edited_draft_message(page) { +async function test_edited_draft_message(page: Page): Promise { await page.waitForSelector(drafts_button, {visible: true}); await page.click(drafts_button); @@ -161,7 +161,7 @@ async function test_edited_draft_message(page) { ); } -async function test_restore_private_message_draft(page) { +async function test_restore_private_message_draft(page: Page): Promise { console.log("Restoring private message draft."); await page.click("#drafts_table .message_row.private-message .restore-draft"); await wait_for_drafts_to_dissapear(page); @@ -180,7 +180,7 @@ async function test_restore_private_message_draft(page) { await page.click("#compose_close"); } -async function test_delete_draft(page) { +async function test_delete_draft(page: Page): Promise { console.log("Deleting draft"); await page.waitForSelector(drafts_button, {visible: true}); await page.click(drafts_button); @@ -194,7 +194,7 @@ async function test_delete_draft(page) { await page.click("body"); } -async function test_save_draft_by_reloading(page) { +async function test_save_draft_by_reloading(page: Page): Promise { console.log("Saving draft by reloading."); await page.keyboard.press("KeyX"); await page.waitForSelector("#private-message", {visible: true}); @@ -229,7 +229,7 @@ async function test_save_draft_by_reloading(page) { ); } -async function test_delete_draft_on_sending(page) { +async function test_delete_draft_on_sending(page: Page): Promise { await page.click("#drafts_table .message_row.private-message .restore-draft"); await wait_for_drafts_to_dissapear(page); await page.waitForSelector("#private-message", {visible: true}); @@ -245,7 +245,7 @@ async function test_delete_draft_on_sending(page) { await page.waitForSelector("#drafts_table .message_row.private-message", {hidden: true}); } -async function drafts_test(page) { +async function drafts_test(page: Page): Promise { await common.log_in(page); await test_empty_drafts(page); diff --git a/frontend_tests/puppeteer_tests/13-delete-message.js b/frontend_tests/puppeteer_tests/13-delete-message.ts similarity index 70% rename from frontend_tests/puppeteer_tests/13-delete-message.js rename to frontend_tests/puppeteer_tests/13-delete-message.ts index ca56c39c85..899f8754ea 100644 --- a/frontend_tests/puppeteer_tests/13-delete-message.js +++ b/frontend_tests/puppeteer_tests/13-delete-message.ts @@ -1,8 +1,8 @@ -"use strict"; +import type {Page} from "puppeteer"; -const common = require("../puppeteer_lib/common"); +import common from "../puppeteer_lib/common"; -async function click_delete_and_return_last_msg_id(page) { +async function click_delete_and_return_last_msg_id(page: Page): Promise { return await page.evaluate(() => { const msg = $("#zhome .message_row").last(); msg.find(".info").trigger("click"); @@ -11,7 +11,7 @@ async function click_delete_and_return_last_msg_id(page) { }); } -async function delete_message_test(page) { +async function delete_message_test(page: Page): Promise { await common.log_in(page); const messages_quantitiy = await page.evaluate(() => $("#zhome .message_row").length); const last_message_id = await click_delete_and_return_last_msg_id(page); @@ -24,12 +24,12 @@ async function delete_message_test(page) { await page.waitForSelector("#do_delete_message_button", {hidden: true}); await page.waitForFunction( - (expected_length) => $("#zhome .message_row").length === expected_length, + (expected_length: number) => $("#zhome .message_row").length === expected_length, {}, messages_quantitiy - 1, ); - await page.waitForSelector(`#${CSS.escape(last_message_id)}`, {hidden: true}); + await page.waitForSelector(`#${CSS.escape(last_message_id!)}`, {hidden: true}); await page.waitForSelector("#do_delete_message_spinner .loading_indicator_spinner", { hidden: true, }); diff --git a/frontend_tests/puppeteer_tests/14-copy-and-paste.js b/frontend_tests/puppeteer_tests/14-copy-and-paste.ts similarity index 80% rename from frontend_tests/puppeteer_tests/14-copy-and-paste.js rename to frontend_tests/puppeteer_tests/14-copy-and-paste.ts index 511ed550b5..92ceb3597d 100644 --- a/frontend_tests/puppeteer_tests/14-copy-and-paste.js +++ b/frontend_tests/puppeteer_tests/14-copy-and-paste.ts @@ -1,13 +1,17 @@ -"use strict"; +import {strict as assert} from "assert"; -const {strict: assert} = require("assert"); +import type {Page} from "puppeteer"; -const common = require("../puppeteer_lib/common"); +import common from "../puppeteer_lib/common"; -async function copy_messages(page, start_message, end_message) { +async function copy_messages( + page: Page, + start_message: string, + end_message: string, +): Promise { return await page.evaluate( - (start_message, end_message) => { - function get_message_node(message) { + (start_message: string, end_message: string) => { + function get_message_node(message: string): Element { return $(`.message_row .message_content:contains("${CSS.escape(message)}")`).get(0); } @@ -15,8 +19,8 @@ async function copy_messages(page, start_message, end_message) { const selectedRange = document.createRange(); selectedRange.setStartAfter(get_message_node(start_message)); selectedRange.setEndBefore(get_message_node(end_message)); - window.getSelection().removeAllRanges(); - window.getSelection().addRange(selectedRange); + window.getSelection()!.removeAllRanges(); + window.getSelection()!.addRange(selectedRange); // Remove existing copy/paste divs, which may linger from the previous // example. (The code clears these out with a zero-second timeout, which @@ -31,32 +35,32 @@ async function copy_messages(page, start_message, end_message) { return temp_div .children("p") .get() - .map((p) => p.textContent); + .map((p) => p.textContent!); }, start_message, end_message, ); } -async function test_copying_first_message_from_topic(page) { +async function test_copying_first_message_from_topic(page: Page): Promise { const actual_copied_lines = await copy_messages(page, "copy paste test C", "copy paste test C"); - const expected_copied_lines = []; + const expected_copied_lines: string[] = []; assert.deepStrictEqual(actual_copied_lines, expected_copied_lines); } -async function test_copying_last_message_from_topic(page) { +async function test_copying_last_message_from_topic(page: Page): Promise { const actual_copied_lines = await copy_messages(page, "copy paste test E", "copy paste test E"); - const expected_copied_lines = []; + const expected_copied_lines: string[] = []; assert.deepStrictEqual(actual_copied_lines, expected_copied_lines); } -async function test_copying_first_two_messages_from_topic(page) { +async function test_copying_first_two_messages_from_topic(page: Page): Promise { 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) { +async function test_copying_all_messages_from_topic(page: Page): Promise { 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", @@ -66,7 +70,7 @@ async function test_copying_all_messages_from_topic(page) { assert.deepStrictEqual(actual_copied_lines, expected_copied_lines); } -async function test_copying_last_from_prev_first_from_next(page) { +async function test_copying_last_from_prev_first_from_next(page: Page): Promise { 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", @@ -77,7 +81,7 @@ async function test_copying_last_from_prev_first_from_next(page) { assert.deepStrictEqual(actual_copied_lines, expected_copied_lines); } -async function test_copying_last_from_prev_all_from_next(page) { +async function test_copying_last_from_prev_all_from_next(page: Page): Promise { 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", @@ -90,7 +94,7 @@ async function test_copying_last_from_prev_all_from_next(page) { assert.deepStrictEqual(actual_copied_lines, expected_copied_lines); } -async function test_copying_all_from_prev_first_from_next(page) { +async function test_copying_all_from_prev_first_from_next(page: Page): Promise { 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", @@ -102,7 +106,7 @@ async function test_copying_all_from_prev_first_from_next(page) { assert.deepStrictEqual(actual_copied_lines, expected_copied_lines); } -async function test_copying_messages_from_several_topics(page) { +async function test_copying_messages_from_several_topics(page: Page): Promise { 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", @@ -117,7 +121,7 @@ async function test_copying_messages_from_several_topics(page) { assert.deepStrictEqual(actual_copied_lines, expected_copied_lines); } -async function copy_paste_test(page) { +async function copy_paste_test(page: Page): Promise { await common.log_in(page); await common.send_multiple_messages(page, [ diff --git a/frontend_tests/puppeteer_tests/15-realm-linkifier.js b/frontend_tests/puppeteer_tests/15-realm-linkifier.ts similarity index 84% rename from frontend_tests/puppeteer_tests/15-realm-linkifier.js rename to frontend_tests/puppeteer_tests/15-realm-linkifier.ts index 8dd7e94aa5..de2f4a4730 100644 --- a/frontend_tests/puppeteer_tests/15-realm-linkifier.js +++ b/frontend_tests/puppeteer_tests/15-realm-linkifier.ts @@ -1,10 +1,10 @@ -"use strict"; +import {strict as assert} from "assert"; -const {strict: assert} = require("assert"); +import type {Page} from "puppeteer"; -const common = require("../puppeteer_lib/common"); +import common from "../puppeteer_lib/common"; -async function test_add_linkifier(page) { +async function test_add_linkifier(page: Page): Promise { await page.waitForSelector(".admin-filter-form", {visible: true}); await common.fill_form(page, "form.admin-filter-form", { pattern: "#(?P[0-9]+)", @@ -31,12 +31,12 @@ async function test_add_linkifier(page) { ); } -async function test_delete_linkifier(page) { +async function test_delete_linkifier(page: Page): Promise { await page.click(".filter_row button"); await page.waitForSelector(".filter_row", {hidden: true}); } -async function test_invalid_linkifier_pattern(page) { +async function test_invalid_linkifier_pattern(page: Page): Promise { await page.waitForSelector(".admin-filter-form", {visible: true}); await common.fill_form(page, "form.admin-filter-form", { pattern: "a$", @@ -51,7 +51,7 @@ async function test_invalid_linkifier_pattern(page) { ); } -async function realm_linkifier_test(page) { +async function realm_linkifier_test(page: Page): Promise { await common.log_in(page); await common.manage_organization(page); await page.click("li[data-section='filter-settings']"); diff --git a/frontend_tests/puppeteer_tests/16-settings.js b/frontend_tests/puppeteer_tests/16-settings.ts similarity index 84% rename from frontend_tests/puppeteer_tests/16-settings.js rename to frontend_tests/puppeteer_tests/16-settings.ts index acffd323f6..e5a385d70a 100644 --- a/frontend_tests/puppeteer_tests/16-settings.js +++ b/frontend_tests/puppeteer_tests/16-settings.ts @@ -1,23 +1,23 @@ -"use strict"; +import {strict as assert} from "assert"; -const {strict: assert} = require("assert"); +import type {Page} from "puppeteer"; -const {test_credentials} = require("../../var/puppeteer/test_credentials"); -const common = require("../puppeteer_lib/common"); +import {test_credentials} from "../../var/puppeteer/test_credentials"; +import common from "../puppeteer_lib/common"; const OUTGOING_WEBHOOK_BOT_TYPE = "3"; const GENERIC_BOT_TYPE = "1"; const zuliprc_regex = /^data:application\/octet-stream;charset=utf-8,\[api]\nemail=.+\nkey=.+\nsite=.+\n$/; -async function get_decoded_url_in_selector(page, selector) { +async function get_decoded_url_in_selector(page: Page, selector: string): Promise { return await page.evaluate( - (selector) => decodeURIComponent($(selector).attr("href")), + (selector: string) => decodeURIComponent($(selector).attr("href")!), selector, ); } -async function open_settings(page) { +async function open_settings(page: Page): Promise { const menu_selector = "#settings-dropdown"; await page.waitForSelector(menu_selector, {visible: true}); await page.click(menu_selector); @@ -31,7 +31,7 @@ async function open_settings(page) { assert(page_url.includes("/#settings/"), `Page url: ${page_url} does not contain /#settings/`); } -async function test_change_full_name(page) { +async function test_change_full_name(page: Page): Promise { await page.click("#change_full_name"); const change_full_name_button_selector = "#change_full_name_button"; @@ -39,7 +39,7 @@ async function test_change_full_name(page) { const full_name_input_selector = 'input[name="full_name"]'; await page.$eval(full_name_input_selector, (el) => { - el.value = ""; + (el as HTMLInputElement).value = ""; }); await page.waitForFunction(() => $(":focus").attr("id") === "change_full_name_modal"); await page.type(full_name_input_selector, "New name"); @@ -47,7 +47,7 @@ async function test_change_full_name(page) { await page.waitForFunction(() => $("#change_full_name").text().trim() === "New name"); } -async function test_change_password(page) { +async function test_change_password(page: Page): Promise { await page.click("#change_password"); const change_password_button_selector = "#change_password_button"; @@ -62,7 +62,7 @@ async function test_change_password(page) { await page.waitForFunction(() => $("#change_password_modal").attr("aria-hidden") === "true"); } -async function test_get_api_key(page) { +async function test_get_api_key(page: Page): Promise { const show_change_api_key_selector = "#api_key_button"; await page.click(show_change_api_key_selector); @@ -85,7 +85,7 @@ async function test_get_api_key(page) { await page.click("#api_key_modal .close"); } -async function test_webhook_bot_creation(page) { +async function test_webhook_bot_creation(page: Page): Promise { await common.fill_form(page, "#create_bot_form", { bot_name: "Bot 1", bot_short_name: "1", @@ -111,7 +111,7 @@ async function test_webhook_bot_creation(page) { ); } -async function test_normal_bot_creation(page) { +async function test_normal_bot_creation(page: Page): Promise { await page.click(".add-a-new-bot-tab"); await page.waitForSelector("#create_bot_button", {visible: true}); @@ -134,7 +134,7 @@ async function test_normal_bot_creation(page) { assert(zuliprc_regex.test(zuliprc_decoded_url), "Incorrect zuliprc format for bot."); } -async function test_botserverrc(page) { +async function test_botserverrc(page: Page): Promise { await page.click("#download_botserverrc"); await page.waitForSelector('#download_botserverrc[href^="data:application"]', {visible: true}); const botserverrc_decoded_url = await get_decoded_url_in_selector( @@ -145,7 +145,7 @@ async function test_botserverrc(page) { assert(botserverrc_regex.test(botserverrc_decoded_url), "Incorrect botserverrc format."); } -async function test_edit_bot_form(page) { +async function test_edit_bot_form(page: Page): Promise { const bot1_email = "1-bot@zulip.testserver"; const bot1_edit_btn = `.open_edit_bot_form[data-email="${CSS.escape(bot1_email)}"]`; await page.click(bot1_edit_btn); @@ -164,14 +164,14 @@ async function test_edit_bot_form(page) { const bot1_name_selector = `.details:has(${bot1_edit_btn}) .name`; await page.waitForFunction( - (bot1_name_selector) => $(bot1_name_selector).text() !== "Bot 1", + (bot1_name_selector: string) => $(bot1_name_selector).text() !== "Bot 1", {}, bot1_name_selector, ); assert.strictEqual(await common.get_text_from_selector(page, bot1_name_selector), "Bot one"); } -async function test_your_bots_section(page) { +async function test_your_bots_section(page: Page): Promise { await page.click('[data-section="your-bots"]'); await test_webhook_bot_creation(page); await test_normal_bot_creation(page); @@ -181,55 +181,61 @@ async function test_your_bots_section(page) { const alert_word_status_selector = "#alert_word_status"; -async function add_alert_word(page, word) { +async function add_alert_word(page: Page, word: string): Promise { await page.type("#create_alert_word_name", word); await page.click("#create_alert_word_button"); } -async function check_alert_word_added(page, word) { +async function check_alert_word_added(page: Page, word: string): Promise { const added_alert_word_selector = `.alert-word-item[data-word='${CSS.escape(word)}']`; await page.waitForSelector(added_alert_word_selector, {visible: true}); } -async function get_alert_words_status_text(page) { +async function get_alert_words_status_text(page: Page): Promise { await page.waitForSelector(alert_word_status_selector, {visible: true}); const status_text = await common.get_text_from_selector(page, ".alert_word_status_text"); return status_text; } -async function close_alert_words_status(page) { +async function close_alert_words_status(page: Page): Promise { const status_close_btn = ".close-alert-word-status"; await page.click(status_close_btn); await page.waitForSelector(alert_word_status_selector, {hidden: true}); } -async function test_and_close_alert_word_added_successfully_status(page, word) { +async function test_and_close_alert_word_added_successfully_status( + page: Page, + word: string, +): Promise { const status_text = await get_alert_words_status_text(page); assert.strictEqual(status_text, `Alert word "${word}" added successfully!`); await close_alert_words_status(page); } -async function test_duplicate_alert_words_cannot_be_added(page, duplicate_word) { +async function test_duplicate_alert_words_cannot_be_added( + page: Page, + duplicate_word: string, +): Promise { await add_alert_word(page, duplicate_word); const status_text = await get_alert_words_status_text(page); assert.strictEqual(status_text, "Alert word already exists!"); await close_alert_words_status(page); } -async function delete_alert_word(page, word) { +async function delete_alert_word(page: Page, word: string): Promise { const delete_btn_selector = `.remove-alert-word[data-word="${CSS.escape(word)}"]`; await page.click(delete_btn_selector); await page.waitForSelector(delete_btn_selector, {hidden: true}); } -async function test_alert_word_deletion(page, word) { +async function test_alert_word_deletion(page: Page, word: string): Promise { await delete_alert_word(page, word); const status_text = await get_alert_words_status_text(page); assert.strictEqual(status_text, "Alert word removed successfully!"); await close_alert_words_status(page); } -async function test_alert_words_section(page) { +async function test_alert_words_section(page: Page): Promise { await page.click('[data-section="alert-words"]'); const word = "puppeteer"; await add_alert_word(page, word); @@ -239,7 +245,7 @@ async function test_alert_words_section(page) { await test_alert_word_deletion(page, word); } -async function change_language(page, language_data_code) { +async function change_language(page: Page, language_data_code: string): Promise { await page.waitForSelector("#default_language", {visible: true}); await page.click("#default_language"); await page.waitForSelector("#default_language_modal", {visible: true}); @@ -247,19 +253,19 @@ async function change_language(page, language_data_code) { await page.click(language_selector); } -async function check_language_setting_status(page) { +async function check_language_setting_status(page: Page): Promise { const language_setting_status_selector = "#language-settings-status"; await page.waitForSelector(language_setting_status_selector, {visible: true}); const status_text = "Saved. Please reload for the change to take effect."; await page.waitForFunction( - (selector, status) => $(selector).text() === status, + (selector: string, status: string) => $(selector).text() === status, {}, language_setting_status_selector, status_text, ); } -async function assert_language_changed_to_chinese(page) { +async function assert_language_changed_to_chinese(page: Page): Promise { await page.waitForSelector("#default_language", {visible: true}); const default_language = await common.get_text_from_selector(page, "#default_language"); assert.strictEqual( @@ -269,7 +275,7 @@ async function assert_language_changed_to_chinese(page) { ); } -async function test_i18n_language_precedence(page) { +async function test_i18n_language_precedence(page: Page): Promise { const settings_url_for_german = "http://zulip.zulipdev.com:9981/de/#settings"; await page.goto(settings_url_for_german); await page.waitForSelector("#settings-change-box", {visible: true}); @@ -277,7 +283,7 @@ async function test_i18n_language_precedence(page) { assert.strictEqual(page_language_code, "de"); } -async function test_default_language_setting(page) { +async function test_default_language_setting(page: Page): Promise { const display_settings_section = '[data-section="display-settings"]'; await page.click(display_settings_section); @@ -304,7 +310,7 @@ async function test_default_language_setting(page) { await page.waitForSelector("#default_language", {visible: true}); } -async function test_notifications_section(page) { +async function test_notifications_section(page: Page): Promise { await page.click('[data-section="notifications"]'); // At the beginning, "PMs, mentions, and alerts"(checkbox name=enable_sounds) audio will be on // and "Streams"(checkbox name=enable_stream_audible_notifications) audio will be off by default. @@ -331,7 +337,7 @@ async function test_notifications_section(page) { */ } -async function settings_tests(page) { +async function settings_tests(page: Page): Promise { await common.log_in(page); await open_settings(page); await test_change_full_name(page); diff --git a/static/js/global.d.ts b/static/js/global.d.ts index bdefb78d03..bd890d1949 100644 --- a/static/js/global.d.ts +++ b/static/js/global.d.ts @@ -146,3 +146,7 @@ declare let user_events: any; declare let user_groups: any; declare let user_pill: any; declare let widgetize: any; + +interface JQuery { + expectOne(): JQuery; +} diff --git a/tools/lib/test_script.py b/tools/lib/test_script.py index 1a5f706be1..e8789a4e08 100644 --- a/tools/lib/test_script.py +++ b/tools/lib/test_script.py @@ -100,7 +100,9 @@ def find_js_test_files(test_dir: str, files: Iterable[str]) -> List[str]: test_files.append(os.path.abspath(file)) if not test_files: - test_files = sorted(glob.glob(os.path.join(test_dir, "*.js"))) + test_files = sorted( + glob.glob(os.path.join(test_dir, "*.ts")) + glob.glob(os.path.join(test_dir, "*.js")) + ) return test_files diff --git a/tools/test-js-with-puppeteer b/tools/test-js-with-puppeteer index b6c5f177e4..6af72b932c 100755 --- a/tools/test-js-with-puppeteer +++ b/tools/test-js-with-puppeteer @@ -61,7 +61,14 @@ def run_tests(files: Iterable[str], external_host: str) -> None: current_test_num = test_number for test_file in test_files[test_number:]: test_name = os.path.basename(test_file) - cmd = ["node", "--unhandled-rejections=strict", test_file] + cmd = [ + "node", + "--unhandled-rejections=strict", + os.path.join(ZULIP_PATH, "node_modules/.bin/ts-node"), + "--script-mode", + "--transpile-only", + test_file, + ] print( "\n\n===================== {}\nRunning {}\n\n".format( test_name, " ".join(map(shlex.quote, cmd)) diff --git a/var/puppeteer/test_credentials.d.ts b/var/puppeteer/test_credentials.d.ts new file mode 100644 index 0000000000..8373b9bd1d --- /dev/null +++ b/var/puppeteer/test_credentials.d.ts @@ -0,0 +1,6 @@ +export const test_credentials: { + default_user: { + username: string; + password: string; + }; +};