Repository: oppia/oppia

Issue:
Migrate acceptance tests to Typescript
The acceptance tests in `core/tests/puppeteer-acceptance-tests` are written in Javascript now. But all the tests including the base implementation and the utility files must be migrated to Typescript.
[BUG]: Support composition when defining users for the acceptance tests.
### Describe the bug

As seen in https://github.com/oppia/oppia/pull/17401#discussion_r1111598923, our structure for giving users multiple roles in e2e tests is a bit limited, because the user classes follow a tight inheritance structure.

It would be better to use a composition-based structure instead. That is, there is a base user with a base set standard capabilities; if we subsequently grant them roles, we should add new functions to that user class in line with the granted roles. This would allow users in an acceptance test to have multiple roles at the same time, while still keeping role-related functionality within specific classes.


Discussion:
I might take this on after my GSoC project.
Moving from high priority to priority since we aren't ready to write acceptance tests due to the pending migration to TS (and we've removed the good first issue label from the checkbox issue), so this is no longer urgent

Resolving patch:
diff --git a/scripts/common.py b/scripts/common.py
--- a/scripts/common.py
+++ b/scripts/common.py
@@ -206,18 +206,15 @@
 ]
 
 ACCEPTANCE_TESTS_SUITE_NAMES = [
-    'blog-admin-tests/assign-roles-to-users-and-change-tag-properties.spec.js',
-    'blog-editor-tests/try-to-publish-a-duplicate-blog-post-and-get-blocked' +
-    '.spec.js',
-    'logged-in-user-tests/click-all-buttons-on-navbar.spec.js',
-    'logged-in-user-tests/click-all-buttons-in-about-page.spec.js',
-    'logged-in-user-tests/click-all-buttons-in-about-foundation-page.spec.js',
-    'logged-in-user-tests/click-all-buttons-in-thanks-for-donating-page' +
-    '.spec.js',
-    'practice-question-admin-tests/add-and-remove-contribution-rights.spec.js',
-    'translation-admin-tests/add-translation-rights.spec.js',
-    'translation-admin-tests/remove-translation-rights.spec.js'
-
+    'blog-admin-tests/assign-roles-to-users-and-change-tag-properties',
+    'blog-editor-tests/try-to-publish-a-duplicate-blog-post-and-get-blocked',
+    'logged-in-user-tests/click-all-buttons-on-navbar',
+    'logged-in-user-tests/click-all-buttons-in-about-page',
+    'logged-in-user-tests/click-all-buttons-in-about-foundation-page',
+    'logged-in-user-tests/click-all-buttons-in-thanks-for-donating-page',
+    'practice-question-admin-tests/add-and-remove-contribution-rights',
+    'translation-admin-tests/add-translation-rights',
+    'translation-admin-tests/remove-translation-rights'
 ]
 
 GAE_PORT_FOR_E2E_TESTING: Final = 8181
diff --git a/scripts/linters/general_purpose_linter.py b/scripts/linters/general_purpose_linter.py
--- a/scripts/linters/general_purpose_linter.py
+++ b/scripts/linters/general_purpose_linter.py
@@ -84,6 +84,7 @@ class BadStringsConstantsDict(TypedDict):
     'core/templates/css/oppia-material.css',
     'core/templates/google-analytics.initializer.ts',
     'extensions/classifiers/proto/*',
+    'core/tests/puppeteer-acceptance-tests/build/*',
     '%s/*' % js_ts_linter.COMPILED_TYPESCRIPT_TMP_PATH)
 
 GENERATED_FILE_PATHS: Final = (
diff --git a/scripts/servers.py b/scripts/servers.py
--- a/scripts/servers.py
+++ b/scripts/servers.py
@@ -737,6 +737,7 @@ def managed_webdriverio_server(
 @contextlib.contextmanager
 def managed_acceptance_tests_server(
     suite_name: str,
+    headless: bool = False,
     stdout: int = subprocess.PIPE,
 ) -> Iterator[psutil.Process]:
     """Returns context manager to start/stop the acceptance tests
@@ -745,6 +746,7 @@ def managed_acceptance_tests_server(
 
     Args:
         suite_name: str. The suite name whose tests should be run.
+        headless: bool. Whether to run the acceptance tests in headless mode.
         stdout: int. This parameter specifies the executed program's standard
             output file handle.
 
@@ -755,24 +757,33 @@ def managed_acceptance_tests_server(
         Exception. The suite_name is not in the list of the acceptance tests
             suite names.
     """
+    if suite_name not in common.ACCEPTANCE_TESTS_SUITE_NAMES:
+        raise Exception('Invalid suite name: %s' % suite_name)
+
+    if headless:
+        os.environ['HEADLESS'] = 'true'
+    else:
+        os.environ['HEADLESS'] = 'false'
+
     nodemodules_jasmine_bin_path = os.path.join(
         common.NODE_MODULES_PATH, '.bin', 'jasmine')
     puppeteer_acceptance_tests_dir_path = os.path.join(
         common.CURR_DIR, 'core', 'tests', 'puppeteer-acceptance-tests')
+    puppeteer_build_dir_path = os.path.join(
+        puppeteer_acceptance_tests_dir_path, 'build')
     spec_dir_path = os.path.join(
-        puppeteer_acceptance_tests_dir_path, 'spec')
+        puppeteer_build_dir_path, 'spec')
     jasmine_config_file_path = os.path.join(
         puppeteer_acceptance_tests_dir_path, 'jasmine.json')
 
+    suite_name_with_extension = '%s.spec.js' % suite_name
+
     acceptance_tests_args = [
         nodemodules_jasmine_bin_path,
         '--config="%s"' % jasmine_config_file_path,
-        '%s' % os.path.join(spec_dir_path, suite_name)
+        '%s' % os.path.join(spec_dir_path, suite_name_with_extension)
     ]
 
-    if suite_name not in common.ACCEPTANCE_TESTS_SUITE_NAMES:
-        raise Exception('Invalid suite name: %s' % suite_name)
-
     # OK to use shell=True here because we are passing string literals,
     # and verifying that the passed suite-name are within the list of
     # the suites we have, so there is no risk of a shell-injection attack.


Test patch:
diff --git a/.github/workflows/e2e_lighthouse_performance_acceptance_tests.yml b/.github/workflows/e2e_lighthouse_performance_acceptance_tests.yml
--- a/.github/workflows/e2e_lighthouse_performance_acceptance_tests.yml
+++ b/.github/workflows/e2e_lighthouse_performance_acceptance_tests.yml
@@ -310,15 +310,15 @@ jobs:
         matrix:
           os: [ubuntu-22.04]
           suite:
-            - blog-admin-tests/assign-roles-to-users-and-change-tag-properties.spec.js
-            - blog-editor-tests/try-to-publish-a-duplicate-blog-post-and-get-blocked.spec.js
-            - logged-in-user-tests/click-all-buttons-on-navbar.spec.js
-            - logged-in-user-tests/click-all-buttons-in-about-page.spec.js
-            - logged-in-user-tests/click-all-buttons-in-about-foundation-page.spec.js
-            - logged-in-user-tests/click-all-buttons-in-thanks-for-donating-page.spec.js
-            - practice-question-admin-tests/add-and-remove-contribution-rights.spec.js
-            - translation-admin-tests/add-translation-rights.spec.js
-            - translation-admin-tests/remove-translation-rights.spec.js
+            - blog-admin-tests/assign-roles-to-users-and-change-tag-properties
+            - blog-editor-tests/try-to-publish-a-duplicate-blog-post-and-get-blocked
+            - logged-in-user-tests/click-all-buttons-on-navbar
+            - logged-in-user-tests/click-all-buttons-in-about-page
+            - logged-in-user-tests/click-all-buttons-in-about-foundation-page
+            - logged-in-user-tests/click-all-buttons-in-thanks-for-donating-page
+            - practice-question-admin-tests/add-and-remove-contribution-rights
+            - translation-admin-tests/add-translation-rights
+            - translation-admin-tests/remove-translation-rights
       steps:
         - uses: actions/checkout@v3
         - uses: actions/setup-python@v3
diff --git a/core/tests/karma.conf.ts b/core/tests/karma.conf.ts
--- a/core/tests/karma.conf.ts
+++ b/core/tests/karma.conf.ts
@@ -62,7 +62,8 @@ module.exports = function(config) {
       'local_compiled_js/core/templates/**/*-e2e.js',
       'local_compiled_js/extensions/**/protractor.js',
       'backend_prod_files/extensions/**',
-      'extensions/classifiers/proto/*'
+      'extensions/classifiers/proto/*',
+      'core/tests/puppeteer-acceptance-tests/*'
     ],
     proxies: {
       // Karma serves files under the /base directory.
diff --git a/core/tests/puppeteer-acceptance-tests/jasmine.json b/core/tests/puppeteer-acceptance-tests/jasmine.json
--- a/core/tests/puppeteer-acceptance-tests/jasmine.json
+++ b/core/tests/puppeteer-acceptance-tests/jasmine.json
@@ -1,5 +1,5 @@
 {
-  "spec_dir": "core/tests/puppeteer-acceptance-tests/spec",
+  "spec_dir": "core/tests/puppeteer-acceptance-tests/build/spec",
   "spec_files": ["**/*.spec.js"],
   "helpers": ["helpers/**/*.js"],
   "env": {
diff --git a/core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/puppeteer-utils.js b/core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/puppeteer-utils.ts
similarity index 64%
rename from core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/puppeteer-utils.js
rename to core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/puppeteer-utils.ts
--- a/core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/puppeteer-utils.js
+++ b/core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/puppeteer-utils.ts
@@ -1,4 +1,4 @@
-// Copyright 2023 The Oppia Authors. All Rights Reserved.
+// Copyright 2024 The Oppia Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -16,8 +16,8 @@
  * @fileoverview Utility File for the Acceptance Tests.
  */
 
-const puppeteer = require('puppeteer');
-const testConstants = require('./test-constants.js');
+import puppeteer, { Page, Browser } from 'puppeteer';
+import testConstants from './test-constants';
 
 const LABEL_FOR_SUBMIT_BUTTON = 'Submit and start contributing';
 /** We accept the empty message because this is what is sent on
@@ -29,26 +29,63 @@ const acceptedBrowserAlerts = [
   'This action is irreversible. Are you sure?'
 ];
 
-module.exports = class baseUser {
-  constructor() {
-    this.page;
-    this.browserObject;
-    this.userHasAcceptedCookies = false;
-  }
+interface IUserProperties {
+  page: Page;
+  browserObject: Browser;
+  userHasAcceptedCookies: boolean;
+  username: string;
+  email: string;
+}
+
+export interface IBaseUser extends IUserProperties {
+  openBrowser: () => Promise<Page>;
+  signInWithEmail: (email: string) => Promise<void>;
+  signUpNewUser: (userName: string, signInEmail: string) => Promise<void>;
+  reloadPage: () => Promise<void>;
+  clickOn: (selector: string) => Promise<void>;
+  type: (selector: string, text: string) => Promise<void>;
+  select: (selector: string, option: string) => Promise<void>;
+  goto: (url: string) => Promise<void>;
+  uploadFile: (filePath: string) => Promise<void>;
+  logout: () => Promise<void>;
+  closeBrowser: () => Promise<void>;
+}
+
+export class BaseUser implements IBaseUser {
+  page!: Page;
+  browserObject!: Browser;
+  userHasAcceptedCookies: boolean = false;
+  email: string = '';
+  username: string = '';
+
+  constructor() {}
 
   /**
    * This is a function that opens a new browser instance for the user.
-   * @returns {Promise<puppeteer.Page>} - Returns a promise that resolves
-   * to a Page object controlled by Puppeteer.
    */
-  async openBrowser() {
+  async openBrowser(): Promise<Page> {
+    const args: string[] = [
+      '--start-fullscreen',
+      '--use-fake-ui-for-media-stream'
+    ];
+
+    const headless = process.env.HEADLESS === 'true';
+    /**
+     * Here we are disabling the site isolation trials because it is causing
+     * tests to fail while running in non headless mode (see
+     * https://github.com/puppeteer/puppeteer/issues/7050).
+     */
+    if (!headless) {
+      args.push('--disable-site-isolation-trials');
+    }
+
     await puppeteer
       .launch({
         /** TODO(#17761): Right now some acceptance tests are failing on
          * headless mode. As per the expected behavior we need to make sure
          * every test passes on both modes. */
-        headless: false,
-        args: ['--start-fullscreen', '--use-fake-ui-for-media-stream']
+        headless,
+        args
       })
       .then(async(browser) => {
         this.browserObject = browser;
@@ -63,15 +100,13 @@ module.exports = class baseUser {
           }
         });
       });
-
     return this.page;
   }
 
   /**
    * Function to sign in the user with the given email to the Oppia website.
-   * @param {string} email - The email of the user.
    */
-  async signInWithEmail(email) {
+  async signInWithEmail(email: string): Promise<void> {
     await this.goto(testConstants.URLs.Home);
     if (!this.userHasAcceptedCookies) {
       await this.clickOn('OK');
@@ -85,32 +120,31 @@ module.exports = class baseUser {
 
   /**
    * This function signs up a new user with the given username and email.
-   * @param {string} userName - The username of the user.
-   * @param {string} signInEmail - The email of the user.
    */
-  async signUpNewUser(userName, signInEmail) {
-    await this.signInWithEmail(signInEmail);
-    await this.type('input.e2e-test-username-input', userName);
+  async signUpNewUser(username: string, email: string): Promise<void> {
+    await this.signInWithEmail(email);
+    await this.type('input.e2e-test-username-input', username);
     await this.clickOn('input.e2e-test-agree-to-terms-checkbox');
     await this.page.waitForSelector(
       'button.e2e-test-register-user:not([disabled])');
     await this.clickOn(LABEL_FOR_SUBMIT_BUTTON);
     await this.page.waitForNavigation({waitUntil: 'networkidle0'});
+
+    this.username = username;
+    this.email = email;
   }
 
   /**
    * Function to reload the current page.
    */
-  async reloadPage() {
+  async reloadPage(): Promise<void> {
     await this.page.reload({waitUntil: ['networkidle0', 'domcontentloaded']});
   }
 
   /**
    * The function clicks the element using the text on the button.
-   * @param {string} selector - The text on the button or the CSS selector of
-   * the element to be clicked
    */
-  async clickOn(selector) {
+  async clickOn(selector: string): Promise<void> {
     try {
       /** Normalize-space is used to remove the extra spaces in the text.
        * Check the documentation for the normalize-space function here :
@@ -126,38 +160,35 @@ module.exports = class baseUser {
 
   /**
    * This function types the text in the input field using its CSS selector.
-   * @param {string} selector - The CSS selector of the input field.
-   * @param {string} text - The text to be typed in the input field.
    */
-  async type(selector, text) {
+  async type(selector: string, text: string): Promise<void> {
     await this.page.waitForSelector(selector);
     await this.page.type(selector, text);
   }
 
   /**
    * This selects a value in a dropdown.
-   * @param {string} selector - The CSS selector of the input field.
-   * @param {string} option - The option to be selected.
    */
-  async select(selector, option) {
+  async select(selector: string, option: string): Promise<void> {
     await this.page.waitForSelector(selector);
     await this.page.select(selector, option);
   }
 
   /**
    * This function navigates to the given URL.
-   * @param {string} url - The URL to which the page has to be navigated.
    */
-  async goto(url) {
+  async goto(url: string): Promise<void> {
     await this.page.goto(url, {waitUntil: 'networkidle0'});
   }
 
   /**
    * This function uploads a file using the given file path.
-   * @param {string} filePath - The path of the file to be uploaded.
    */
-  async uploadFile(filePath) {
+  async uploadFile(filePath: string): Promise<void> {
     const inputUploadHandle = await this.page.$('input[type=file]');
+    if (inputUploadHandle === null) {
+      throw new Error('No file input found while attempting to upload a file.');
+    }
     let fileToUpload = filePath;
     inputUploadHandle.uploadFile(fileToUpload);
   }
@@ -165,7 +196,7 @@ module.exports = class baseUser {
   /**
    * This function logs out the current user.
    */
-  async logout() {
+  async logout(): Promise<void> {
     await this.goto(testConstants.URLs.Logout);
     await this.page.waitForSelector(testConstants.Dashboard.MainDashboard);
   }
@@ -173,7 +204,9 @@ module.exports = class baseUser {
   /**
    * This function closes the current Puppeteer browser instance.
    */
-  async closeBrowser() {
+  async closeBrowser(): Promise<void> {
     await this.browserObject.close();
   }
-};
+}
+
+export const BaseUserFactory = (): IBaseUser => new BaseUser();
diff --git a/core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/show-message-utils.js b/core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/show-message-utils.ts
similarity index 85%
rename from core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/show-message-utils.js
rename to core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/show-message-utils.ts
--- a/core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/show-message-utils.js
+++ b/core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/show-message-utils.ts
@@ -1,4 +1,4 @@
-// Copyright 2023 The Oppia Authors. All Rights Reserved.
+// Copyright 2024 The Oppia Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -16,10 +16,8 @@
  * @fileoverview Utility function for logging the progress of the tests.
  */
 
-let showMessage = function(message) {
+export let showMessage = function(message: string): void {
   // We use console statements to log the progress or feedback of the tests.
   // eslint-disable-next-line no-console
   console.log('LOG: ' + message);
 };
-
-module.exports = { showMessage };
diff --git a/core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/test-constants.js b/core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/test-constants.ts
similarity index 86%
rename from core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/test-constants.js
rename to core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/test-constants.ts
--- a/core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/test-constants.js
+++ b/core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/test-constants.ts
@@ -1,4 +1,4 @@
-// Copyright 2023 The Oppia Authors. All Rights Reserved.
+// Copyright 2024 The Oppia Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -16,9 +16,9 @@
  * @fileoverview Constants that can be re-used in the accpetance tests.
  */
 
-const path = require('path');
+import path from 'path';
 
-let testConstants = {
+export default {
   URLs: {
     About: 'http://localhost:8181/about',
     AboutFoundation: 'http://localhost:8181/about-foundation',
@@ -52,11 +52,19 @@ let testConstants = {
   SignInDetails: {
     inputField: 'input.e2e-test-sign-in-email-input',
   },
+  Roles: {
+    TRANSLATION_ADMIN: 'translation admin',
+    BLOG_ADMIN: 'blog admin',
+    BLOG_POST_EDITOR: 'blog post editor',
+    QUESTION_ADMIN: 'question admin'
+  } as const,
+  BlogRights: {
+    BLOG_ADMIN: 'BLOG_ADMIN',
+    BLOG_POST_EDITOR: 'BLOG_POST_EDITOR'
+  } as const,
   images: {
     blogPostThumbnailImage: path.resolve(
       __dirname, '../images/blog-post-thumbnail.svg')
   },
-  DEFAULT_SPEC_TIMEOUT: 300000
+  DEFAULT_SPEC_TIMEOUT: 300000,
 };
-
-module.exports = testConstants;
diff --git a/core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/user-factory.js b/core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/user-factory.js
deleted file mode 100644
--- a/core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/user-factory.js
+++ /dev/null
@@ -1,199 +0,0 @@
-// Copyright 2023 The Oppia Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS-IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-/**
- * @fileoverview Utility File for declaring and initializing users.
- */
-
-let e2eSuperAdmin = require('../user-utilities/super-admin-utils.js');
-let e2eGuestUser = require('../user-utilities/logged-in-users-utils.js');
-let e2eBlogAdmin = e2eBlogPostEditor = require(
-  '../user-utilities/blog-post-admin-utils.js');
-let e2eTranslationAdmin = require(
-  '../user-utilities/translation-admin-utils.js');
-let e2eQuestionAdmin = require(
-  '../user-utilities/question-admin-utils.js');
-
-/**
- * Global user instances that are created and can be reused again.
- */
-let superAdminInstance = null;
-let activeUsers = [];
-const ROLE_BLOG_ADMIN = 'blog admin';
-const ROLE_BLOG_POST_EDITOR = 'blog post editor';
-const ROLE_TRANSLATION_ADMIN = 'translation admin';
-const ROLE_QUESTION_ADMIN = 'question admin';
-
-/**
- * The function creates a new super admin user and returns the instance
- * of that user.
- * @param {string} username - The username of the super admin.
- * @returns The super admin instance created.
- */
-let createNewSuperAdmin = async function(username) {
-  if (superAdminInstance !== null) {
-    return superAdminInstance;
-  }
-
-  const superAdmin = new e2eSuperAdmin();
-  await superAdmin.openBrowser();
-  await superAdmin.signUpNewUser(username, 'testadmin@example.com');
-
-  activeUsers.push(superAdmin);
-  superAdminInstance = superAdmin;
-  return superAdmin;
-};
-
-/**
- * The function creates a new blog admin user and returns the instance
- * of that user.
- * @param {string} username - The username of the blog admin.
- * @returns The blog admin instance created.
- */
-let createNewBlogAdmin = async function(username) {
-  if (superAdminInstance === null) {
-    superAdminInstance = await createNewSuperAdmin('superAdm');
-  }
-
-  const blogAdmin = new e2eBlogAdmin();
-  await blogAdmin.openBrowser();
-  await blogAdmin.signUpNewUser(username, 'blog_admin@example.com');
-
-  await superAdminInstance.assignRoleToUser(username, ROLE_BLOG_ADMIN);
-  await superAdminInstance.expectUserToHaveRole(username, ROLE_BLOG_ADMIN);
-
-  activeUsers.push(blogAdmin);
-  return blogAdmin;
-};
-
-/**
- * The function creates a new blog post editor user and returns the
- * instance of that user.
- * @param {string} username - The username of the blog post editor.
- * @returns The blog post editor instance created.
- */
-let createNewBlogPostEditor = async function(username) {
-  const blogAdmin = await createNewBlogAdmin('blogAdm');
-
-  const blogPostEditor = new e2eBlogPostEditor();
-  await blogPostEditor.openBrowser();
-  await blogPostEditor.signUpNewUser(
-    username, 'blog_post_editor@example.com');
-
-  await blogAdmin.assignUserToRoleFromBlogAdminPage(
-    username, 'BLOG_POST_EDITOR');
-  await superAdminInstance.expectUserToHaveRole(
-    username, ROLE_BLOG_POST_EDITOR);
-
-  activeUsers.push(blogPostEditor);
-  return blogPostEditor;
-};
-
-/**
- * The function creates a new guest user and returns the instance of that user.
- * @param {string} username - The username of the guest user.
- * @param {string} email - The email of the guest user.
- * @returns The guest user instance created.
- */
-let createNewGuestUser = async function(username, email) {
-  const guestUser = new e2eGuestUser();
-  await guestUser.openBrowser();
-  await guestUser.signUpNewUser(username, email);
-
-  activeUsers.push(guestUser);
-  return guestUser;
-};
-
-/**
- * Function to create a user with the translation admin role.
- * @param {string} username - the username of the translation admin.
- * @param {string} email - the email of the user.
- * @returns the instance of the translation admin.
- */
-let createNewTranslationAdmin = async function(username, email) {
-  if (superAdminInstance === null) {
-    superAdminInstance = await createNewSuperAdmin('superAdm');
-  }
-
-  const translationAdmin = new e2eTranslationAdmin();
-  await translationAdmin.openBrowser();
-  await translationAdmin.signUpNewUser(
-    username,
-    'translation_admin@example.com');
-
-  await superAdminInstance.assignRoleToUser(
-    username, ROLE_TRANSLATION_ADMIN);
-  await superAdminInstance.expectUserToHaveRole(
-    username, ROLE_TRANSLATION_ADMIN);
-
-  activeUsers.push(translationAdmin);
-  return translationAdmin;
-};
-
-/**
- * Function to create a user with the question admin role.
- * @param {string} username - the username of the question admin.
- * @param {string} email - the email of the user.
- * @returns the instance of the question admin.
- */
-let createNewQuestionAdmin = async function(username, email) {
-  if (superAdminInstance === null) {
-    superAdminInstance = await createNewSuperAdmin('superAdm');
-  }
-
-  const questionAdmin = new e2eQuestionAdmin();
-  await questionAdmin.openBrowser();
-  await questionAdmin.signUpNewUser(
-    username,
-    'question_admin@example.com');
-
-  await superAdminInstance.assignRoleToUser(
-    username, ROLE_QUESTION_ADMIN);
-  await superAdminInstance.expectUserToHaveRole(
-    username, ROLE_QUESTION_ADMIN);
-
-  activeUsers.push(questionAdmin);
-  return questionAdmin;
-};
-
-/**
- * The function closes all the browsers opened by different users.
- */
-let closeAllBrowsers = async function() {
-  for (let i = 0; i < activeUsers.length; i++) {
-    await activeUsers[i].closeBrowser();
-  }
-};
-
-/**
- * Function to close the browser opened by the specified user
- * @param {baseUser} user - the user object of the user who's browser
- * should be closed.
- */
-let closeBrowserForUser = async function(user) {
-  const index = activeUsers.indexOf(user);
-  activeUsers.splice(index, 1);
-  await user.closeBrowser();
-};
-
-module.exports = {
-  createNewSuperAdmin,
-  createNewBlogAdmin,
-  createNewBlogPostEditor,
-  createNewGuestUser,
-  createNewTranslationAdmin,
-  createNewQuestionAdmin,
-  closeAllBrowsers,
-  closeBrowserForUser
-};
diff --git a/core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/user-factory.ts b/core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/user-factory.ts
new file mode 100644
--- /dev/null
+++ b/core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/user-factory.ts
@@ -0,0 +1,183 @@
+// Copyright 2024 The Oppia Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Utility File for declaring and initializing users.
+ */
+
+import { SuperAdminFactory, ISuperAdmin } from '../user-utilities/super-admin-utils';
+import { BaseUserFactory, IBaseUser } from './puppeteer-utils';
+import { TranslationAdminFactory } from '../user-utilities/translation-admin-utils';
+import { LoggedInUserFactory, ILoggedInUser } from '../user-utilities/logged-in-users-utils';
+import { BlogAdminFactory, IBlogAdmin } from '../user-utilities/blog-admin-utils';
+import { QuestionAdminFactory } from '../user-utilities/question-admin-utils';
+import { BlogPostEditorFactory } from '../user-utilities/blog-post-editor-utils';
+import testConstants from './test-constants';
+
+const ROLES = testConstants.Roles;
+const BLOG_RIGHTS = testConstants.BlogRights;
+
+/**
+ * Mapping of user roles to their respective function class.
+ */
+const USER_ROLE_MAPPING = {
+  [ROLES.TRANSLATION_ADMIN]: TranslationAdminFactory,
+  [ROLES.BLOG_ADMIN]: BlogAdminFactory,
+  [ROLES.BLOG_POST_EDITOR]: BlogPostEditorFactory,
+  [ROLES.QUESTION_ADMIN]: QuestionAdminFactory
+} as const;
+
+/**
+ * These types are used to create a union of all the roles and then
+ * create an intersection of all the roles. This is used to create a
+ * composition of the user and the role for type inference.
+ */
+type UnionToIntersection<U> =
+  (U extends IBaseUser ? (k: U) => void : never) extends
+    ((k: infer I) => void) ? I : never;
+
+type MultipleRoleIntersection<T extends (keyof typeof USER_ROLE_MAPPING)[]> =
+  UnionToIntersection<ReturnType<typeof USER_ROLE_MAPPING[T[number]]>>;
+
+type OptionalRoles<TRoles extends (keyof typeof USER_ROLE_MAPPING)[]> =
+  TRoles extends never[] ? [] : TRoles | [];
+
+/**
+ * Global user instances that are created and can be reused again.
+ */
+let superAdminInstance: ISuperAdmin & IBlogAdmin | null = null;
+let activeUsers: IBaseUser[] = [];
+
+export class UserFactory {
+  /**
+   * This function creates a composition of the user and the role
+   * through object prototypes and returns the instance of that user.
+   */
+  private static composeUserWithRole = function<
+    TUser extends IBaseUser,
+    TRole extends IBaseUser
+  >(
+      user: TUser,
+      role: TRole
+  ): TUser & TRole {
+    const userPrototype = Object.getPrototypeOf(user);
+    const rolePrototype = Object.getPrototypeOf(role);
+
+    Object.getOwnPropertyNames(rolePrototype).forEach((name: string) => {
+      Object.defineProperty(
+        userPrototype,
+        name,
+        Object.getOwnPropertyDescriptor(rolePrototype, name) ||
+          Object.create(null)
+      );
+    });
+
+    return user as TUser & TRole;
+  };
+
+  /**
+   * This function assigns roles to a user and returns the instance of
+   * that user.
+   */
+  static assignRolesToUser = async function<
+    TUser extends IBaseUser,
+    TRoles extends (keyof typeof USER_ROLE_MAPPING)[]
+  >(
+      user: TUser,
+      roles: TRoles
+  ): Promise<TUser & MultipleRoleIntersection<TRoles>> {
+    for (const role of roles) {
+      if (superAdminInstance === null) {
+        superAdminInstance = await UserFactory.createNewSuperAdmin('superAdm');
+      }
+
+      switch (role) {
+        case ROLES.BLOG_POST_EDITOR:
+          await superAdminInstance.assignUserToRoleFromBlogAdminPage(
+            user.username, BLOG_RIGHTS.BLOG_POST_EDITOR);
+          break;
+        default:
+          await superAdminInstance.assignRoleToUser(user.username, role);
+          break;
+      }
+
+      await superAdminInstance.expectUserToHaveRole(user.username, role);
+
+      UserFactory.composeUserWithRole(user, USER_ROLE_MAPPING[role]());
+    }
+
+    return user as TUser & MultipleRoleIntersection<typeof roles>;
+  };
+
+  /**
+   * This function creates a new user and returns the instance of that user.
+   */
+  static createNewUser = async function<
+    TRoles extends (keyof typeof USER_ROLE_MAPPING)[] = never[]
+  >(
+      username: string, email: string,
+      roles: OptionalRoles<TRoles> = [] as OptionalRoles<TRoles>
+  ): Promise<ILoggedInUser & MultipleRoleIntersection<TRoles>> {
+    let user = UserFactory.composeUserWithRole(
+      BaseUserFactory(), LoggedInUserFactory());
+    await user.openBrowser();
+    await user.signUpNewUser(username, email);
+    activeUsers.push(user);
+
+    return await UserFactory.assignRolesToUser(user, roles);
+  };
+
+  /**
+   * The function creates a new super admin user and returns the instance
+   * of that user.
+   */
+  static createNewSuperAdmin = async function(
+      username: string
+  ): Promise<ISuperAdmin & IBlogAdmin> {
+    if (superAdminInstance !== null) {
+      return superAdminInstance;
+    }
+
+    const user = await UserFactory.createNewUser(
+      username, 'testadmin@example.com');
+    const superAdmin = UserFactory.composeUserWithRole(
+      user, SuperAdminFactory());
+    await superAdmin.assignRoleToUser(username, ROLES.BLOG_ADMIN);
+    await superAdmin.expectUserToHaveRole(username, ROLES.BLOG_ADMIN);
+    superAdminInstance = UserFactory.composeUserWithRole(
+      superAdmin, BlogAdminFactory());
+
+    return superAdminInstance;
+  };
+
+  /**
+   * This function closes all the browsers opened by different users.
+   */
+  static closeAllBrowsers = async function(): Promise<void> {
+    for (let i = 0; i < activeUsers.length; i++) {
+      await activeUsers[i].closeBrowser();
+    }
+  };
+
+  /**
+   * This function closes the browser for the provided user.
+   */
+  static closeBrowserForUser = async function(
+      user: IBaseUser
+  ): Promise<void> {
+    const index = activeUsers.indexOf(user);
+    activeUsers.splice(index, 1);
+    await user.closeBrowser();
+  };
+}
diff --git a/core/tests/puppeteer-acceptance-tests/spec/blog-admin-tests/assign-roles-to-users-and-change-tag-properties.spec.js b/core/tests/puppeteer-acceptance-tests/spec/blog-admin-tests/assign-roles-to-users-and-change-tag-properties.spec.ts
similarity index 55%
rename from core/tests/puppeteer-acceptance-tests/spec/blog-admin-tests/assign-roles-to-users-and-change-tag-properties.spec.js
rename to core/tests/puppeteer-acceptance-tests/spec/blog-admin-tests/assign-roles-to-users-and-change-tag-properties.spec.ts
--- a/core/tests/puppeteer-acceptance-tests/spec/blog-admin-tests/assign-roles-to-users-and-change-tag-properties.spec.js
+++ b/core/tests/puppeteer-acceptance-tests/spec/blog-admin-tests/assign-roles-to-users-and-change-tag-properties.spec.ts
@@ -1,4 +1,4 @@
-// Copyright 2023 The Oppia Authors. All Rights Reserved.
+// Copyright 2024 The Oppia Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -16,22 +16,25 @@
  * @fileoverview Acceptance Test for Blog Admin
  */
 
-const userFactory = require(
-  '../../puppeteer-testing-utilities/user-factory.js');
-const testConstants = require(
-  '../../puppeteer-testing-utilities/test-constants.js');
+import { UserFactory } from
+  '../../puppeteer-testing-utilities/user-factory';
+import testConstants from
+  '../../puppeteer-testing-utilities/test-constants';
+import { IBlogAdmin } from '../../user-utilities/blog-admin-utils';
+import { ISuperAdmin } from '../../user-utilities/super-admin-utils';
 
 const DEFAULT_SPEC_TIMEOUT = testConstants.DEFAULT_SPEC_TIMEOUT;
+const ROLES = testConstants.Roles;
+const BLOG_RIGHTS = testConstants.BlogRights;
 
 describe('Blog Admin', function() {
-  const ROLE_BLOG_ADMIN = 'blog admin';
-  const ROLE_BLOG_POST_EDITOR = 'blog post editor';
-  let superAdmin = null;
-  let blogAdmin = null;
+  let superAdmin: ISuperAdmin;
+  let blogAdmin: IBlogAdmin;
 
   beforeAll(async function() {
-    superAdmin = await userFactory.createNewSuperAdmin('superAdm');
-    blogAdmin = await userFactory.createNewBlogAdmin('blogAdm');
+    superAdmin = await UserFactory.createNewSuperAdmin('superAdm');
+    blogAdmin = await UserFactory.createNewUser(
+      'blogAdm', 'blog_admin@example.com', [ROLES.BLOG_ADMIN]);
   }, DEFAULT_SPEC_TIMEOUT);
 
   /** TODO(#17162): This test should be done without the need of super admin, as
@@ -40,34 +43,35 @@ describe('Blog Admin', function() {
   */
   it('should assign roles to users and change tag properties',
     async function() {
-      const guestUsr1 = await userFactory.createNewGuestUser(
+      const guestUsr1 = await UserFactory.createNewUser(
         'guestUsr1', 'guest_user1@example.com');
-      const guestUsr2 = await userFactory.createNewGuestUser(
+      const guestUsr2 = await UserFactory.createNewUser(
         'guestUsr2', 'guest_user2@example.com');
 
-      await superAdmin.expectUserNotToHaveRole('guestUsr1', ROLE_BLOG_ADMIN);
+      await superAdmin.expectUserNotToHaveRole('guestUsr1', ROLES.BLOG_ADMIN);
       await blogAdmin.assignUserToRoleFromBlogAdminPage(
-        'guestUsr1', 'BLOG_ADMIN');
-      await superAdmin.expectUserToHaveRole('guestUsr1', ROLE_BLOG_ADMIN);
+        'guestUsr1', BLOG_RIGHTS.BLOG_ADMIN);
+      await superAdmin.expectUserToHaveRole('guestUsr1', ROLES.BLOG_ADMIN);
 
       await superAdmin.expectUserNotToHaveRole(
-        'guestUsr2', ROLE_BLOG_POST_EDITOR);
+        'guestUsr2', ROLES.BLOG_POST_EDITOR);
       await blogAdmin.assignUserToRoleFromBlogAdminPage(
-        'guestUsr2', 'BLOG_POST_EDITOR');
-      await superAdmin.expectUserToHaveRole('guestUsr2', ROLE_BLOG_POST_EDITOR);
+        'guestUsr2', BLOG_RIGHTS.BLOG_POST_EDITOR);
+      await superAdmin.expectUserToHaveRole(
+        'guestUsr2', ROLES.BLOG_POST_EDITOR);
 
       await blogAdmin.removeBlogEditorRoleFromUsername('guestUsr2');
       await superAdmin.expectUserNotToHaveRole(
-        'guestUsr2', ROLE_BLOG_POST_EDITOR);
+        'guestUsr2', ROLES.BLOG_POST_EDITOR);
 
-      await blogAdmin.expectMaximumTagLimitNotToBe('5');
-      await blogAdmin.setMaximumTagLimitTo('5');
-      await blogAdmin.expectMaximumTagLimitToBe('5');
+      await blogAdmin.expectMaximumTagLimitNotToBe(5);
+      await blogAdmin.setMaximumTagLimitTo(5);
+      await blogAdmin.expectMaximumTagLimitToBe(5);
       await guestUsr1.closeBrowser();
       await guestUsr2.closeBrowser();
     }, DEFAULT_SPEC_TIMEOUT);
 
   afterAll(async function() {
-    await userFactory.closeAllBrowsers();
+    await UserFactory.closeAllBrowsers();
   });
 });
diff --git a/core/tests/puppeteer-acceptance-tests/spec/blog-editor-tests/try-to-publish-a-duplicate-blog-post-and-get-blocked.spec.js b/core/tests/puppeteer-acceptance-tests/spec/blog-editor-tests/try-to-publish-a-duplicate-blog-post-and-get-blocked.spec.ts
similarity index 75%
rename from core/tests/puppeteer-acceptance-tests/spec/blog-editor-tests/try-to-publish-a-duplicate-blog-post-and-get-blocked.spec.js
rename to core/tests/puppeteer-acceptance-tests/spec/blog-editor-tests/try-to-publish-a-duplicate-blog-post-and-get-blocked.spec.ts
--- a/core/tests/puppeteer-acceptance-tests/spec/blog-editor-tests/try-to-publish-a-duplicate-blog-post-and-get-blocked.spec.js
+++ b/core/tests/puppeteer-acceptance-tests/spec/blog-editor-tests/try-to-publish-a-duplicate-blog-post-and-get-blocked.spec.ts
@@ -1,4 +1,4 @@
-// Copyright 2023 The Oppia Authors. All Rights Reserved.
+// Copyright 2024 The Oppia Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -16,21 +16,24 @@
  * @fileoverview Acceptance Test for Blog Post Editor
  */
 
-const userFactory = require(
-  '../../puppeteer-testing-utilities/user-factory.js');
-const testConstants = require(
-  '../../puppeteer-testing-utilities/test-constants.js');
+import { UserFactory } from
+  '../../puppeteer-testing-utilities/user-factory';
+import testConstants from
+  '../../puppeteer-testing-utilities/test-constants';
+import { IBlogPostEditor } from '../../user-utilities/blog-post-editor-utils';
 
 const DEFAULT_SPEC_TIMEOUT = testConstants.DEFAULT_SPEC_TIMEOUT;
+const ROLES = testConstants.Roles;
 const duplicateBlogPostWarning = ' Blog Post with the' +
   ' given title exists already. Please use a different title. ';
 
 describe('Blog Editor', function() {
-  let blogPostEditor = null;
+  let blogPostEditor: IBlogPostEditor;
 
   beforeAll(async function() {
-    blogPostEditor = await userFactory.createNewBlogPostEditor(
-      'blogPostEditor');
+    blogPostEditor = await UserFactory.createNewUser(
+      'blogPostEditor', 'blog_post_editor@example.com',
+      [ROLES.BLOG_POST_EDITOR]);
   }, DEFAULT_SPEC_TIMEOUT);
 
   it('should check blog editor unable to publish duplicate blog post',
@@ -52,6 +55,6 @@ describe('Blog Editor', function() {
     }, DEFAULT_SPEC_TIMEOUT);
 
   afterAll(async function() {
-    await userFactory.closeAllBrowsers();
+    await UserFactory.closeAllBrowsers();
   });
 });
diff --git a/core/tests/puppeteer-acceptance-tests/spec/helpers/reporter.js b/core/tests/puppeteer-acceptance-tests/spec/helpers/reporter.ts
similarity index 70%
rename from core/tests/puppeteer-acceptance-tests/spec/helpers/reporter.js
rename to core/tests/puppeteer-acceptance-tests/spec/helpers/reporter.ts
--- a/core/tests/puppeteer-acceptance-tests/spec/helpers/reporter.js
+++ b/core/tests/puppeteer-acceptance-tests/spec/helpers/reporter.ts
@@ -1,4 +1,4 @@
-// Copyright 2023 The Oppia Authors. All Rights Reserved.
+// Copyright 2024 The Oppia Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -16,15 +16,22 @@
  * @fileoverview Custom jasmine reporter for acceptance tests
  */
 
-const util = require('util');
+import util from 'util';
+import sourceMapSupport from 'source-map-support';
 
-let suiteCount,
-  specCount,
-  executedSpecCount,
-  failureCount,
-  failedSpecs = [],
-  pendingSpecs = [],
-  failedSuites = [],
+sourceMapSupport.install();
+
+interface SuiteFailureResult extends jasmine.JasmineDoneInfo {
+  fullName: string;
+}
+
+let suiteCount: number,
+  specCount: number,
+  executedSpecCount: number,
+  failureCount: number,
+  failedSpecs: jasmine.SpecResult[] = [],
+  pendingSpecs: jasmine.SpecResult[] = [],
+  failedSuites: jasmine.SuiteResult[] = [],
   ansi = {
     green: '\x1B[32;40m',
     red: '\x1B[31;40m',
@@ -33,30 +40,35 @@ let suiteCount,
     none: '\x1B[0m'
   };
 
-let print = function() {
-  process.stdout.write(util.format.apply(this, arguments));
+let print = function(message: string): void {
+  process.stdout.write(util.format(message));
 };
 
-let printNewline = function() {
+let printNewline = function(): void {
   print('\n');
 };
 
-let colored = function(color, str) {
+let colored = function(color: keyof typeof ansi, str: string): string {
   return (ansi[color] + str + ansi.none);
 };
 
-let printLog = function(result) {
+let printFailures = function(
+    result: jasmine.JasmineDoneInfo | jasmine.SpecResult | jasmine.SuiteResult
+): void {
   for (let i = 0; i < result.failedExpectations.length; i++) {
-    const failedExpectation = result.failedExpectations[i];
     printNewline();
     print('Message:');
     printNewline();
-    print(colored('red', failedExpectation.message));
+    print(colored('red', result.failedExpectations[i].message));
     printNewline();
     print('Stack:');
     printNewline();
-    print(!failedExpectation.stack ? '' : failedExpectation.stack);
+    print(result.failedExpectations[i].stack ?? '');
   }
+};
+
+let printSpecLog = function(result: jasmine.SpecResult): void {
+  printFailures(result);
 
   if (result.failedExpectations.length === 0 &&
     result.passedExpectations.length === 0) {
@@ -69,7 +81,18 @@ let printLog = function(result) {
   printNewline();
 };
 
-let pendingSpecTrace = function(result, pendingSpecNumber) {
+let printSuiteLog = function(
+    result: jasmine.JasmineDoneInfo | jasmine.SuiteResult
+): void {
+  printFailures(result);
+
+  printNewline();
+};
+
+let pendingSpecTrace = function(
+    result: jasmine.SpecResult,
+    pendingSpecNumber: number
+): void {
   printNewline();
   print(pendingSpecNumber + '. ' + result.fullName);
   printNewline();
@@ -83,31 +106,25 @@ let pendingSpecTrace = function(result, pendingSpecNumber) {
   printNewline();
 };
 
-let specFailureTrace = function(result, failedSpecNumber) {
+let specFailureTrace = function(
+    result: jasmine.SpecResult,
+    failedSpecNumber: number
+): void {
   printNewline();
   print(failedSpecNumber + '. ' + result.fullName);
-  printLog(result);
-
-  if (result.trace) {
-    printNewline();
-    print('Trace:');
-    printNewline();
-
-    for (const entry of result.trace) {
-      print(`${entry.timestamp}ms: ${entry.message}`);
-      printNewline();
-    }
-  }
+  printSpecLog(result);
 };
 
-let suiteFailureTrace = function(result) {
+let suiteFailureTrace = function(
+    result: SuiteFailureResult | jasmine.SuiteResult
+): void {
   printNewline();
   print('Suite error: ' + result.fullName);
-  printLog(result);
+  printSuiteLog(result);
 };
 
-const Reporter = {
-  jasmineStarted: function(suiteInfo) {
+const Reporter: jasmine.CustomReporter = {
+  jasmineStarted: function(suiteInfo: jasmine.JasmineStartedInfo): void {
     suiteCount = 0;
     specCount = 0;
     executedSpecCount = 0;
@@ -117,7 +134,7 @@ const Reporter = {
     printNewline();
   },
 
-  suiteStarted: function(result) {
+  suiteStarted: function(result: jasmine.SuiteResult): void {
     suiteCount++;
     const heading = '. Suite started: ';
     const length = suiteCount.toString().length + heading.length +
@@ -135,15 +152,15 @@ const Reporter = {
     printNewline();
   },
 
-  specStarted: function(result) {
+  specStarted: function(result: jasmine.SpecResult): void {
     printNewline();
     print('Spec started : ' + result.fullName);
     printNewline();
   },
 
-  specDone: function(result) {
+  specDone: function(result: jasmine.SpecResult) {
     specCount++;
-    const seconds = result ? result.duration / 1000 : 0;
+    const seconds = result?.duration ? result.duration / 1000 : 0;
 
     switch (result.status) {
       case 'pending':
@@ -176,14 +193,14 @@ const Reporter = {
     printNewline();
   },
 
-  suiteDone: function(result) {
+  suiteDone: function(result: jasmine.SuiteResult): void {
     if (result.failedExpectations && result.failedExpectations.length > 0) {
       failureCount++;
       failedSuites.push(result);
     }
   },
 
-  jasmineDone: function(result) {
+  jasmineDone: function(result: jasmine.JasmineDoneInfo): void {
     printNewline();
     printNewline();
     if (failedSpecs.length > 0) {
@@ -205,7 +222,7 @@ const Reporter = {
     if (pendingSpecs.length > 0) {
       print('Pending:');
     }
-    for (i = 0; i < pendingSpecs.length; i++) {
+    for (let i = 0; i < pendingSpecs.length; i++) {
       pendingSpecTrace(pendingSpecs[i], i + 1);
     }
 
diff --git a/core/tests/puppeteer-acceptance-tests/spec/logged-in-user-tests/click-all-buttons-in-about-foundation-page.spec.js b/core/tests/puppeteer-acceptance-tests/spec/logged-in-user-tests/click-all-buttons-in-about-foundation-page.spec.ts
similarity index 87%
rename from core/tests/puppeteer-acceptance-tests/spec/logged-in-user-tests/click-all-buttons-in-about-foundation-page.spec.js
rename to core/tests/puppeteer-acceptance-tests/spec/logged-in-user-tests/click-all-buttons-in-about-foundation-page.spec.ts
--- a/core/tests/puppeteer-acceptance-tests/spec/logged-in-user-tests/click-all-buttons-in-about-foundation-page.spec.js
+++ b/core/tests/puppeteer-acceptance-tests/spec/logged-in-user-tests/click-all-buttons-in-about-foundation-page.spec.ts
@@ -1,4 +1,4 @@
-// Copyright 2023 The Oppia Authors. All Rights Reserved.
+// Copyright 2024 The Oppia Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -17,18 +17,19 @@
  * can open links by clicking all buttons in about foundation page
  */
 
-const userFactory = require(
-  '../../puppeteer-testing-utilities/user-factory.js');
-const testConstants = require(
-  '../../puppeteer-testing-utilities/test-constants.js');
+import { UserFactory } from
+  '../../puppeteer-testing-utilities/user-factory';
+import { ILoggedInUser } from '../../user-utilities/logged-in-users-utils';
+import testConstants from
+  '../../puppeteer-testing-utilities/test-constants';
 
 const DEFAULT_SPEC_TIMEOUT = testConstants.DEFAULT_SPEC_TIMEOUT;
 
 describe('Logged-in User in About Foundation page', function() {
-  let testUser = null;
+  let testUser: ILoggedInUser;
 
   beforeAll(async function() {
-    testUser = await userFactory.createNewGuestUser(
+    testUser = await UserFactory.createNewUser(
       'testuser', 'testuser@example.com');
   }, DEFAULT_SPEC_TIMEOUT);
 
@@ -84,6 +85,6 @@ describe('Logged-in User in About Foundation page', function() {
     }, DEFAULT_SPEC_TIMEOUT);
 
   afterAll(async function() {
-    await userFactory.closeAllBrowsers();
+    await UserFactory.closeAllBrowsers();
   });
 });
diff --git a/core/tests/puppeteer-acceptance-tests/spec/logged-in-user-tests/click-all-buttons-in-about-page.spec.js b/core/tests/puppeteer-acceptance-tests/spec/logged-in-user-tests/click-all-buttons-in-about-page.spec.ts
similarity index 84%
rename from core/tests/puppeteer-acceptance-tests/spec/logged-in-user-tests/click-all-buttons-in-about-page.spec.js
rename to core/tests/puppeteer-acceptance-tests/spec/logged-in-user-tests/click-all-buttons-in-about-page.spec.ts
--- a/core/tests/puppeteer-acceptance-tests/spec/logged-in-user-tests/click-all-buttons-in-about-page.spec.js
+++ b/core/tests/puppeteer-acceptance-tests/spec/logged-in-user-tests/click-all-buttons-in-about-page.spec.ts
@@ -1,4 +1,4 @@
-// Copyright 2023 The Oppia Authors. All Rights Reserved.
+// Copyright 2024 The Oppia Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -17,18 +17,19 @@
  * can open links by clicking all buttons in about foundation page
  */
 
-const userFactory = require(
-  '../../puppeteer-testing-utilities/user-factory.js');
-const testConstants = require(
-  '../../puppeteer-testing-utilities/test-constants.js');
+import { UserFactory } from
+  '../../puppeteer-testing-utilities/user-factory';
+import { ILoggedInUser } from '../../user-utilities/logged-in-users-utils';
+import testConstants from
+  '../../puppeteer-testing-utilities/test-constants';
 
 const DEFAULT_SPEC_TIMEOUT = testConstants.DEFAULT_SPEC_TIMEOUT;
 
 describe('Logged-in User in About page', function() {
-  let testUser = null;
+  let testUser: ILoggedInUser;
 
   beforeAll(async function() {
-    testUser = await userFactory.createNewGuestUser(
+    testUser = await UserFactory.createNewUser(
       'testuser', 'testuser@example.com');
   }, DEFAULT_SPEC_TIMEOUT);
 
@@ -68,6 +69,6 @@ describe('Logged-in User in About page', function() {
     }, DEFAULT_SPEC_TIMEOUT);
 
   afterAll(async function() {
-    await userFactory.closeAllBrowsers();
+    await UserFactory.closeAllBrowsers();
   });
 });
diff --git a/core/tests/puppeteer-acceptance-tests/spec/logged-in-user-tests/click-all-buttons-in-thanks-for-donating-page.spec.js b/core/tests/puppeteer-acceptance-tests/spec/logged-in-user-tests/click-all-buttons-in-thanks-for-donating-page.spec.ts
similarity index 80%
rename from core/tests/puppeteer-acceptance-tests/spec/logged-in-user-tests/click-all-buttons-in-thanks-for-donating-page.spec.js
rename to core/tests/puppeteer-acceptance-tests/spec/logged-in-user-tests/click-all-buttons-in-thanks-for-donating-page.spec.ts
--- a/core/tests/puppeteer-acceptance-tests/spec/logged-in-user-tests/click-all-buttons-in-thanks-for-donating-page.spec.js
+++ b/core/tests/puppeteer-acceptance-tests/spec/logged-in-user-tests/click-all-buttons-in-thanks-for-donating-page.spec.ts
@@ -1,4 +1,4 @@
-// Copyright 2023 The Oppia Authors. All Rights Reserved.
+// Copyright 2024 The Oppia Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -17,18 +17,19 @@
  * can open links by clicking all buttons in thanks for donating page
  */
 
-const userFactory = require(
-  '../../puppeteer-testing-utilities/user-factory.js');
-const testConstants = require(
-  '../../puppeteer-testing-utilities/test-constants.js');
+import { UserFactory } from
+  '../../puppeteer-testing-utilities/user-factory';
+import { ILoggedInUser } from '../../user-utilities/logged-in-users-utils';
+import testConstants from
+  '../../puppeteer-testing-utilities/test-constants';
 
 const DEFAULT_SPEC_TIMEOUT = testConstants.DEFAULT_SPEC_TIMEOUT;
 
 describe('Logged-in User in Thanks for Donating page', function() {
-  let testUser = null;
+  let testUser: ILoggedInUser;
 
   beforeAll(async function() {
-    testUser = await userFactory.createNewGuestUser(
+    testUser = await UserFactory.createNewUser(
       'testuser', 'testuser@example.com');
   }, DEFAULT_SPEC_TIMEOUT);
 
@@ -53,6 +54,6 @@ describe('Logged-in User in Thanks for Donating page', function() {
   }, DEFAULT_SPEC_TIMEOUT);
 
   afterAll(async function() {
-    await userFactory.closeAllBrowsers();
+    await UserFactory.closeAllBrowsers();
   });
 });
diff --git a/core/tests/puppeteer-acceptance-tests/spec/logged-in-user-tests/click-all-buttons-on-navbar.spec.js b/core/tests/puppeteer-acceptance-tests/spec/logged-in-user-tests/click-all-buttons-on-navbar.spec.ts
similarity index 86%
rename from core/tests/puppeteer-acceptance-tests/spec/logged-in-user-tests/click-all-buttons-on-navbar.spec.js
rename to core/tests/puppeteer-acceptance-tests/spec/logged-in-user-tests/click-all-buttons-on-navbar.spec.ts
--- a/core/tests/puppeteer-acceptance-tests/spec/logged-in-user-tests/click-all-buttons-on-navbar.spec.js
+++ b/core/tests/puppeteer-acceptance-tests/spec/logged-in-user-tests/click-all-buttons-on-navbar.spec.ts
@@ -1,4 +1,4 @@
-// Copyright 2023 The Oppia Authors. All Rights Reserved.
+// Copyright 2024 The Oppia Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -17,18 +17,19 @@
  * can open link by clicking all buttons on navbar
  */
 
-const userFactory = require(
-  '../../puppeteer-testing-utilities/user-factory.js');
-const testConstants = require(
-  '../../puppeteer-testing-utilities/test-constants.js');
+import { UserFactory } from
+  '../../puppeteer-testing-utilities/user-factory';
+import { ILoggedInUser } from '../../user-utilities/logged-in-users-utils';
+import testConstants from
+  '../../puppeteer-testing-utilities/test-constants';
 
 const DEFAULT_SPEC_TIMEOUT = testConstants.DEFAULT_SPEC_TIMEOUT;
 
 describe('Logged-in User', function() {
-  let testUser = null;
+  let testUser: ILoggedInUser;
 
   beforeAll(async function() {
-    testUser = await userFactory.createNewGuestUser(
+    testUser = await UserFactory.createNewUser(
       'testuser', 'testuser@example.com');
   }, DEFAULT_SPEC_TIMEOUT);
 
@@ -77,6 +78,6 @@ describe('Logged-in User', function() {
     }, DEFAULT_SPEC_TIMEOUT);
 
   afterAll(async function() {
-    await userFactory.closeAllBrowsers();
+    await UserFactory.closeAllBrowsers();
   });
 });
diff --git a/core/tests/puppeteer-acceptance-tests/spec/practice-question-admin-tests/add-and-remove-contribution-rights.spec.js b/core/tests/puppeteer-acceptance-tests/spec/practice-question-admin-tests/add-and-remove-contribution-rights.spec.ts
similarity index 77%
rename from core/tests/puppeteer-acceptance-tests/spec/practice-question-admin-tests/add-and-remove-contribution-rights.spec.js
rename to core/tests/puppeteer-acceptance-tests/spec/practice-question-admin-tests/add-and-remove-contribution-rights.spec.ts
--- a/core/tests/puppeteer-acceptance-tests/spec/practice-question-admin-tests/add-and-remove-contribution-rights.spec.js
+++ b/core/tests/puppeteer-acceptance-tests/spec/practice-question-admin-tests/add-and-remove-contribution-rights.spec.ts
@@ -1,4 +1,4 @@
-// Copyright 2023 The Oppia Authors. All Rights Reserved.
+// Copyright 2024 The Oppia Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -17,26 +17,28 @@
  * reviewing question rights to users.
  */
 
-const userFactory = require(
-  '../../puppeteer-testing-utilities/user-factory.js');
-const testConstants = require(
-  '../../puppeteer-testing-utilities/test-constants.js');
+import { UserFactory } from
+  '../../puppeteer-testing-utilities/user-factory';
+import { IQuestionAdmin } from '../../user-utilities/question-admin-utils';
+import testConstants from
+  '../../puppeteer-testing-utilities/test-constants';
 
 const DEFAULT_SPEC_TIMEOUT = testConstants.DEFAULT_SPEC_TIMEOUT;
+const ROLES = testConstants.Roles;
 
 describe('Question Admin', function() {
-  let questionAdmin = null;
+  let questionAdmin: IQuestionAdmin;
 
   beforeAll(async function() {
-    questionAdmin =
-      await userFactory.createNewQuestionAdmin('questionAdm');
+    questionAdmin = await UserFactory.createNewUser(
+      'questionAdm', 'question_admin@example.com', [ROLES.QUESTION_ADMIN]);
   }, DEFAULT_SPEC_TIMEOUT);
 
   it('should be able to provide rights to review and submit questions to user.',
     async function() {
-      let Tester = await userFactory.createNewGuestUser(
+      let Tester = await UserFactory.createNewUser(
         'Tester', 'admin.tester@example.com');
-      await userFactory.closeBrowserForUser(Tester);
+      await UserFactory.closeBrowserForUser(Tester);
 
       await questionAdmin.navigateToContributorDashboardAdminPage();
       await questionAdmin.verifyUserCannotReviewQuestions('Tester');
@@ -62,6 +64,6 @@ describe('Question Admin', function() {
     }, DEFAULT_SPEC_TIMEOUT);
 
   afterAll(async function() {
-    await userFactory.closeAllBrowsers();
+    await UserFactory.closeAllBrowsers();
   });
 });
diff --git a/core/tests/puppeteer-acceptance-tests/spec/translation-admin-tests/add-translation-rights.spec.js b/core/tests/puppeteer-acceptance-tests/spec/translation-admin-tests/add-translation-rights.spec.ts
similarity index 69%
rename from core/tests/puppeteer-acceptance-tests/spec/translation-admin-tests/add-translation-rights.spec.js
rename to core/tests/puppeteer-acceptance-tests/spec/translation-admin-tests/add-translation-rights.spec.ts
--- a/core/tests/puppeteer-acceptance-tests/spec/translation-admin-tests/add-translation-rights.spec.js
+++ b/core/tests/puppeteer-acceptance-tests/spec/translation-admin-tests/add-translation-rights.spec.ts
@@ -1,4 +1,4 @@
-// Copyright 2023 The Oppia Authors. All Rights Reserved.
+// Copyright 2024 The Oppia Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -17,26 +17,28 @@
  * rights to users.
  */
 
-const userFactory = require(
-  '../../puppeteer-testing-utilities/user-factory.js');
-const testConstants = require(
-  '../../puppeteer-testing-utilities/test-constants.js');
+import { UserFactory } from
+  '../../puppeteer-testing-utilities/user-factory';
+import { ITranslationAdmin } from '../../user-utilities/translation-admin-utils';
+import testConstants from '../../puppeteer-testing-utilities/test-constants';
 
 const DEFAULT_SPEC_TIMEOUT = testConstants.DEFAULT_SPEC_TIMEOUT;
+const ROLES = testConstants.Roles;
 
 describe('Translation Admin', function() {
-  let translationAdmin = null;
+  let translationAdmin: ITranslationAdmin;
 
   beforeAll(async function() {
-    translationAdmin =
-      await userFactory.createNewTranslationAdmin('translationAdm');
+    translationAdmin = await UserFactory.createNewUser(
+      'translationAdm', 'translation_admin@example.com',
+      [ROLES.TRANSLATION_ADMIN]);
   }, DEFAULT_SPEC_TIMEOUT);
 
   it('should be able to provide translation rights to user.',
     async function() {
-      let translatorSpanish = await userFactory.createNewGuestUser(
+      let translatorSpanish = await UserFactory.createNewUser(
         'translatorSpanish', 'translatorSpanish@example.com');
-      await userFactory.closeBrowserForUser(translatorSpanish);
+      await UserFactory.closeBrowserForUser(translatorSpanish);
       await translationAdmin.navigateToContributorDashboardAdminPage();
 
       await translationAdmin.addTranslationLanguageReviewRights(
@@ -50,6 +52,6 @@ describe('Translation Admin', function() {
     }, DEFAULT_SPEC_TIMEOUT);
 
   afterAll(async function() {
-    await userFactory.closeAllBrowsers();
+    await UserFactory.closeAllBrowsers();
   });
 });
diff --git a/core/tests/puppeteer-acceptance-tests/spec/translation-admin-tests/remove-translation-rights.spec.js b/core/tests/puppeteer-acceptance-tests/spec/translation-admin-tests/remove-translation-rights.spec.ts
similarity index 70%
rename from core/tests/puppeteer-acceptance-tests/spec/translation-admin-tests/remove-translation-rights.spec.js
rename to core/tests/puppeteer-acceptance-tests/spec/translation-admin-tests/remove-translation-rights.spec.ts
--- a/core/tests/puppeteer-acceptance-tests/spec/translation-admin-tests/remove-translation-rights.spec.js
+++ b/core/tests/puppeteer-acceptance-tests/spec/translation-admin-tests/remove-translation-rights.spec.ts
@@ -1,4 +1,4 @@
-// Copyright 2023 The Oppia Authors. All Rights Reserved.
+// Copyright 2024 The Oppia Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -17,26 +17,29 @@
  * rights from users.
  */
 
-const userFactory = require(
-  '../../puppeteer-testing-utilities/user-factory.js');
-const testConstants = require(
-  '../../puppeteer-testing-utilities/test-constants.js');
+import { UserFactory } from
+  '../../puppeteer-testing-utilities/user-factory';
+import { ITranslationAdmin } from '../../user-utilities/translation-admin-utils';
+import testConstants from
+  '../../puppeteer-testing-utilities/test-constants';
 
 const DEFAULT_SPEC_TIMEOUT = testConstants.DEFAULT_SPEC_TIMEOUT;
+const ROLES = testConstants.Roles;
 
 describe('Translation Admin', function() {
-  let translationAdmin = null;
+  let translationAdmin: ITranslationAdmin;
 
   beforeAll(async function() {
-    translationAdmin =
-      await userFactory.createNewTranslationAdmin('translationAdm');
+    translationAdmin = await UserFactory.createNewUser(
+      'translationAdm', 'translation_admin@example.com',
+      [ROLES.TRANSLATION_ADMIN]);
   }, DEFAULT_SPEC_TIMEOUT);
 
   it('should be able to remove translation rights from user.',
     async function() {
-      let translatorSpanish = await userFactory.createNewGuestUser(
+      let translatorSpanish = await UserFactory.createNewUser(
         'translatorSpanish', 'translatorSpanish@example.com');
-      await userFactory.closeBrowserForUser(translatorSpanish);
+      await UserFactory.closeBrowserForUser(translatorSpanish);
       await translationAdmin.navigateToContributorDashboardAdminPage();
       await translationAdmin.addTranslationLanguageReviewRights(
         'translatorSpanish', 'es');
@@ -53,6 +56,6 @@ describe('Translation Admin', function() {
     }, DEFAULT_SPEC_TIMEOUT);
 
   afterAll(async function() {
-    await userFactory.closeAllBrowsers();
+    await UserFactory.closeAllBrowsers();
   });
 });
diff --git a/core/tests/puppeteer-acceptance-tests/user-utilities/blog-admin-utils.ts b/core/tests/puppeteer-acceptance-tests/user-utilities/blog-admin-utils.ts
new file mode 100644
--- /dev/null
+++ b/core/tests/puppeteer-acceptance-tests/user-utilities/blog-admin-utils.ts
@@ -0,0 +1,110 @@
+// Copyright 2024 The Oppia Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Blog Admin users utility file.
+ */
+
+import { IBaseUser, BaseUser } from
+  '../puppeteer-testing-utilities/puppeteer-utils';
+import testConstants from
+  '../puppeteer-testing-utilities/test-constants';
+import { showMessage } from
+  '../puppeteer-testing-utilities/show-message-utils';
+
+const BLOG_RIGHTS = testConstants.BlogRights;
+
+const roleUpdateUsernameInput = 'input#label-target-update-form-name';
+const blogEditorUsernameInput = 'input#label-target-form-reviewer-username';
+const maximumTagLimitInput = 'input#float-input';
+const blogAdminUrl = testConstants.URLs.BlogAdmin;
+
+const LABEL_FOR_SAVE_BUTTON = 'Save';
+
+export interface IBlogAdmin extends IBaseUser {
+  assignUserToRoleFromBlogAdminPage: (
+    username: string, role: keyof typeof BLOG_RIGHTS) => Promise<void>;
+  removeBlogEditorRoleFromUsername: (
+    username: string) => Promise<void>;
+  setMaximumTagLimitTo: (limit: number) => Promise<void>;
+  expectMaximumTagLimitNotToBe: (limit: number) => Promise<void>;
+  expectMaximumTagLimitToBe: (limit: number) => Promise<void>;
+}
+
+class BlogAdmin extends BaseUser implements IBlogAdmin {
+  /**
+   * This function assigns a user with a role from the blog admin page.
+   */
+  async assignUserToRoleFromBlogAdminPage(
+      username: string,
+      role: keyof typeof BLOG_RIGHTS
+  ): Promise<void> {
+    await this.goto(blogAdminUrl);
+    await this.page.select('select#label-target-update-form-role-select', role);
+    await this.type(roleUpdateUsernameInput, username);
+    await this.clickOn('button.oppia-blog-admin-update-role-button');
+  }
+
+  /**
+   * This function removes blog editor role from the users.
+   */
+  async removeBlogEditorRoleFromUsername(username: string): Promise<void> {
+    await this.goto(blogAdminUrl);
+    await this.type(blogEditorUsernameInput, username);
+    await this.clickOn('button.oppia-blog-admin-remove-blog-editor-button');
+  }
+
+  /**
+   * This function changes the blog tags limit.
+   */
+  async setMaximumTagLimitTo(limit: number): Promise<void> {
+    // These steps are for deleting the existing value in the input field.
+    const tagInputField = await this.page.$(maximumTagLimitInput);
+    await tagInputField?.click({ clickCount: 3 });
+    await this.page.keyboard.press('Backspace');
+
+    await this.type(maximumTagLimitInput, limit.toString());
+    await this.clickOn(LABEL_FOR_SAVE_BUTTON);
+
+    showMessage(`Successfully updated the tag limit to ${limit}!`);
+  }
+
+  /**
+   * This function checks if the tag limit is not equal to.
+   */
+  async expectMaximumTagLimitNotToBe(limit: number): Promise<void> {
+    const tagLimit = await this.page.$eval(
+      maximumTagLimitInput,
+      element => (element as HTMLInputElement).value);
+    if (parseInt(tagLimit) === limit) {
+      throw new Error(`Maximum tag limit is already ${limit}!`);
+    }
+    showMessage(`Maximum tag limit is not ${limit}!`);
+  }
+
+  /**
+   * This function checks if the tag limit is equal to.
+   */
+  async expectMaximumTagLimitToBe(limit: number): Promise<void> {
+    const tagLimit = await this.page.$eval(
+      maximumTagLimitInput,
+      element => (element as HTMLInputElement).value);
+    if (parseInt(tagLimit) !== limit) {
+      throw new Error(`Maximum tag limit is not ${limit}!`);
+    }
+    showMessage(`Maximum tag is currently ${limit}!`);
+  }
+}
+
+export let BlogAdminFactory = (): IBlogAdmin => new BlogAdmin();
diff --git a/core/tests/puppeteer-acceptance-tests/user-utilities/blog-post-admin-utils.js b/core/tests/puppeteer-acceptance-tests/user-utilities/blog-post-admin-utils.js
deleted file mode 100644
--- a/core/tests/puppeteer-acceptance-tests/user-utilities/blog-post-admin-utils.js
+++ /dev/null
@@ -1,448 +0,0 @@
-// Copyright 2023 The Oppia Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS-IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-/**
- * @fileoverview Blog Admin users utility file.
- */
-
-const baseUser = require(
-  '../puppeteer-testing-utilities/puppeteer-utils.js');
-const testConstants = require(
-  '../puppeteer-testing-utilities/test-constants.js');
-const { showMessage } = require(
-  '../puppeteer-testing-utilities/show-message-utils.js');
-
-const blogTitleInput = 'input.e2e-test-blog-post-title-field';
-const blogBodyInput = 'div.e2e-test-rte';
-const thumbnailPhotoBox = 'div.e2e-test-photo-clickable';
-const unauthErrorContainer = 'div.e2e-test-error-container';
-const blogDashboardAuthorDetailsModal = 'div.modal-dialog';
-const roleUpdateUsernameInput = 'input#label-target-update-form-name';
-const blogEditorUsernameInput = 'input#label-target-form-reviewer-username';
-const maximumTagLimitInput = 'input#float-input';
-const blogAuthorBioField = 'textarea.e2e-test-blog-author-bio-field';
-const blogDashboardUrl = testConstants.URLs.BlogDashboard;
-const blogAdminUrl = testConstants.URLs.BlogAdmin;
-const publishBlogPostButton = 'button.e2e-test-publish-blog-post-button';
-const addThumbnailImageButton = 'button.e2e-test-photo-upload-submit';
-const blogPostThumbnailImage = testConstants.images.blogPostThumbnailImage;
-
-const LABEL_FOR_NEW_BLOG_POST_CREATE_BUTTON = 'CREATE NEW BLOG POST';
-const LABEL_FOR_SAVE_BUTTON = 'Save';
-const LABEL_FOR_DONE_BUTTON = 'DONE';
-const LABEL_FOR_SAVE_DRAFT_BUTTON = 'SAVE AS DRAFT';
-const LABEL_FOR_DELETE_BUTTON = 'Delete';
-const LABEL_FOR_CONFIRM_BUTTON = 'Confirm';
-
-module.exports = class e2eBlogPostAdmin extends baseUser {
-  /**
-   * Function for adding blog post author bio in blog dashboard.
-   */
-  async addUserBioInBlogDashboard() {
-    await this.type(blogAuthorBioField, 'Dummy-User-Bio');
-    await this.page.waitForSelector(
-      'button.e2e-test-save-author-details-button:not([disabled])');
-    await this.clickOn(LABEL_FOR_SAVE_BUTTON);
-  }
-
-  /**
-   * Function for navigating to the blog dashboard page.
-   */
-  async navigateToBlogDashboardPage() {
-    await this.goto(blogDashboardUrl);
-  }
-
-  /**
-   * This function creates a blog post with given title.
-   * @param {string} draftBlogPostTitle - The title of the draft blog post
-   * to be created.
-   */
-  async createDraftBlogPostWithTitle(draftBlogPostTitle) {
-    await this.addUserBioInBlogDashboard();
-    /** Giving explicit timeout because we need to wait for small
-     * transition to complete. We cannot wait for the next element to click
-     * using its selector as it is instantly loaded in the DOM but cannot
-     * be clicked until the transition is completed.
-     */
-    await this.page.waitForTimeout(500);
-    await this.clickOn(LABEL_FOR_NEW_BLOG_POST_CREATE_BUTTON);
-    await this.type(blogTitleInput, draftBlogPostTitle);
-    await this.page.keyboard.press('Tab');
-    await this.type(blogBodyInput, 'test blog post body content');
-    await this.clickOn(LABEL_FOR_DONE_BUTTON);
-    await this.page.waitForSelector(
-      'button.e2e-test-save-as-draft-button:not([disabled])');
-    await this.clickOn(LABEL_FOR_SAVE_DRAFT_BUTTON);
-
-    showMessage('Successfully created a draft blog post!');
-    await this.goto(blogDashboardUrl);
-  }
-
-  /**
-   * This function deletes a draft blog post with given title.
-   * @param {string} draftBlogPostTitle - The title of the draft blog post.
-   */
-  async deleteDraftBlogPostWithTitle(draftBlogPostTitle) {
-    await this.page.exposeFunction('deleteDraftBlogPost', async() => {
-    /** Giving explicit timeout because we need to wait for small
-     * transition to complete. We cannot wait for the next element to click
-     * using its selector as it is instantly loaded in the DOM but cannot
-     * be clicked until the transition is completed.
-     */
-      await this.page.waitForTimeout(100);
-      await this.clickOn(LABEL_FOR_DELETE_BUTTON);
-      await this.page.waitForSelector('div.modal-dialog');
-      await this.clickOn(LABEL_FOR_CONFIRM_BUTTON);
-      showMessage('Draft blog post with given title deleted successfully!');
-    });
-    await this.page.evaluate(async({draftBlogPostTitle}) => {
-      const allDraftBlogPosts = document.getElementsByClassName(
-        'blog-dashboard-tile-content');
-      for (let i = 0; i < allDraftBlogPosts.length; i++) {
-        let checkDraftBlogPostTitle = allDraftBlogPosts[i].
-          getElementsByClassName('e2e-test-blog-post-title')[0].innerText;
-        if (draftBlogPostTitle === checkDraftBlogPostTitle) {
-          allDraftBlogPosts[i].getElementsByClassName(
-            'e2e-test-blog-post-edit-box')[0].click();
-          await window.deleteDraftBlogPost();
-          return;
-        }
-      }
-    }, {draftBlogPostTitle});
-  }
-
-  /**
-   * This function checks if the Publish button is disabled.
-   */
-  async expectPublishButtonToBeDisabled() {
-    await this.page.waitForSelector(publishBlogPostButton);
-    await this.page.evaluate(() => {
-      const publishedButtonIsDisabled = document.getElementsByClassName(
-        'e2e-test-publish-blog-post-button')[0].disabled;
-      if (!publishedButtonIsDisabled) {
-        throw new Error(
-          'Published button is not disabled when the blog post data is not' +
-          ' completely filled');
-      }
-    });
-    showMessage(
-      'Published button is disabled when blog post data is not completely' +
-      ' filled');
-  }
-
-  /**
-   * This function publishes a blog post with given title.
-   * @param {string} newBlogPostTitle - The title of the blog post
-   * to be published.
-   */
-  async publishNewBlogPostWithTitle(newBlogPostTitle) {
-    await this.addUserBioInBlogDashboard();
-    /** Giving explicit timeout because we need to wait for small
-     * transition to complete. We cannot wait for the next element to click
-     * using its selector as it is instantly loaded in the DOM but cannot
-     * be clicked until the transition is completed.
-     */
-    await this.page.waitForTimeout(500);
-    await this.clickOn(LABEL_FOR_NEW_BLOG_POST_CREATE_BUTTON);
-
-    await this.expectPublishButtonToBeDisabled();
-    await this.clickOn('button.mat-button-toggle-button');
-    await this.expectPublishButtonToBeDisabled();
-    await this.clickOn(thumbnailPhotoBox);
-    await this.uploadFile(blogPostThumbnailImage);
-    await this.page.waitForSelector(
-      `${addThumbnailImageButton}:not([disabled])`);
-    await this.clickOn(addThumbnailImageButton);
-    await this.page.waitForSelector('body.modal-open', {hidden: true});
-    await this.expectPublishButtonToBeDisabled();
-
-    await this.type(blogTitleInput, newBlogPostTitle);
-    await this.page.keyboard.press('Tab');
-    await this.type(blogBodyInput, 'test blog post body content');
-    await this.clickOn(LABEL_FOR_DONE_BUTTON);
-
-    await this.page.waitForSelector(
-      `${publishBlogPostButton}:not([disabled])`);
-    await this.clickOn('PUBLISH');
-    await this.page.waitForSelector('button.e2e-test-confirm-button');
-    await this.clickOn(LABEL_FOR_CONFIRM_BUTTON);
-    showMessage('Successfully published a blog post!');
-  }
-
-  /**
-   * This function creates a new blog post with the given title.
-   * @param {string} newBlogPostTitle - The title of the blog post.
-   */
-  async createNewBlogPostWithTitle(newBlogPostTitle) {
-    await this.clickOn('NEW POST');
-    await this.clickOn('button.mat-button-toggle-button');
-    await this.clickOn(thumbnailPhotoBox);
-    await this.uploadFile(blogPostThumbnailImage);
-    await this.page.waitForSelector(
-      `${addThumbnailImageButton}:not([disabled])`);
-    await this.clickOn(addThumbnailImageButton);
-    await this.page.waitForSelector('body.modal-open', {hidden: true});
-
-    await this.type(blogTitleInput, newBlogPostTitle);
-    await this.page.keyboard.press('Tab');
-    await this.type(blogBodyInput, 'test blog post body content - duplicate');
-    await this.clickOn(LABEL_FOR_DONE_BUTTON);
-  }
-
-  /**
-   * This function deletes a published blog post with the given title.
-   * @param {string} blogPostTitle - The title of the published blog post
-   * to be deleted.
-   */
-  async deletePublishedBlogPostWithTitle(blogPostTitle) {
-    await this.clickOn('PUBLISHED');
-    await this.page.exposeFunction('deletePublishedBlogPost', async() => {
-    /** Giving explicit timeout because we need to wait for small
-     * transition to complete. We cannot wait for the next element to click
-     * using its selector as it is instantly loaded in the DOM but cannot
-     * be clicked until the transition is completed.
-     */
-      await this.page.waitForTimeout(100);
-      await this.clickOn(LABEL_FOR_DELETE_BUTTON);
-      await this.page.waitForSelector('button.e2e-test-confirm-button');
-      await this.clickOn(LABEL_FOR_CONFIRM_BUTTON);
-      showMessage('Published blog post with given title deleted successfully!');
-    });
-    await this.page.evaluate(async(blogPostTitle) => {
-      const allPublishedBlogPosts = document.getElementsByClassName(
-        'blog-dashboard-tile-content');
-      for (let i = 0; i < allPublishedBlogPosts.length; i++) {
-        let publishedBlogPostTitle = allPublishedBlogPosts[i].
-          getElementsByClassName('e2e-test-blog-post-title')[0].innerText;
-        if (publishedBlogPostTitle === blogPostTitle) {
-          allPublishedBlogPosts[i].getElementsByClassName(
-            'e2e-test-blog-post-edit-box')[0].click();
-          await window.deletePublishedBlogPost();
-          return;
-        }
-      }
-    }, blogPostTitle);
-  }
-
-  /**
-   * This function checks that the user is unable to publish a blog post.
-   */
-  async expectUserUnableToPublishBlogPost(expectedWarningMessage) {
-    const toastMessageBox = await this.page.$(
-      'div.e2e-test-toast-warning-message');
-    const toastMessageWarning = await this.page.evaluate(
-      el => el.textContent, toastMessageBox);
-    const isPublishButtonDisabled = await this.page.$eval(
-      publishBlogPostButton, (button) => {
-        return button.disabled;
-      });
-
-    if (!isPublishButtonDisabled) {
-      throw new Error('User is able to publish the blog post');
-    }
-    if (expectedWarningMessage !== toastMessageWarning) {
-      throw new Error(
-        'Expected warning message is not same as the actual warning message\n' +
-        `Expected warning: ${expectedWarningMessage}\n` +
-        `Displayed warning: ${toastMessageWarning}\n`);
-    }
-
-    showMessage(
-      'User is unable to publish the blog post because ' + toastMessageWarning);
-  }
-
-  /**
-   * This function checks the number of the blog posts in the blog dashboard.
-   * @param {number} number - The number of the blog posts.
-   */
-  async expectNumberOfBlogPostsToBe(number) {
-    await this.page.evaluate(async(number) => {
-      const allBlogPosts = document.getElementsByClassName(
-        'blog-dashboard-tile-content');
-      if (allBlogPosts.length !== number) {
-        throw new Error(
-          `Number of blog posts is not equal to ${number}`);
-      }
-    }, number);
-
-    showMessage(`Number of blog posts is equal to ${number}`);
-  }
-
-  /**
-   * This function navigates to the Published tab in the blog-dashbaord.
-   */
-  async navigateToPublishTab() {
-    await this.goto(blogDashboardUrl);
-    await this.clickOn('PUBLISHED');
-    showMessage('Navigated to publish tab.');
-  }
-
-  /**
-   * This function checks a draft blog post to be created with the given title.
-   * @param {string} checkDraftBlogPostByTitle - The title of draft blog post.
-   */
-  async expectDraftBlogPostWithTitleToBePresent(checkDraftBlogPostByTitle) {
-    await this.goto(blogDashboardUrl);
-    await this.page.evaluate(async(checkDraftBlogPostByTitle) => {
-      const allDraftBlogPosts = document.getElementsByClassName(
-        'blog-dashboard-tile-content');
-      let count = 0;
-      for (let i = 0; i < allDraftBlogPosts.length; i++) {
-        let draftBlogPostTitle = allDraftBlogPosts[i].getElementsByClassName(
-          'e2e-test-blog-post-title')[0].innerText;
-        if (draftBlogPostTitle === checkDraftBlogPostByTitle) {
-          count++;
-        }
-      }
-      if (count === 0) {
-        throw new Error(
-          `Draft blog post with title ${checkDraftBlogPostByTitle} does not` +
-          ' exist!');
-      } else if (count > 1) {
-        throw new Error(
-          `Draft blog post with title ${checkDraftBlogPostByTitle} exists` +
-          ' more than once!');
-      }
-    }, checkDraftBlogPostByTitle);
-    showMessage(
-      `Draft blog post with title ${checkDraftBlogPostByTitle} exists!`);
-  }
-
-  /**
-   * This function checks if the blog post with given title is published.
-   * @param {string} blogPostTitle - The title of the blog post.
-   */
-  async expectPublishedBlogPostWithTitleToBePresent(blogPostTitle) {
-    await this.goto(blogDashboardUrl);
-    await this.clickOn('PUBLISHED');
-    await this.page.evaluate(async(blogPostTitle) => {
-      const allPublishedBlogPosts = document.getElementsByClassName(
-        'blog-dashboard-tile-content');
-      let count = 0;
-      for (let i = 0; i < allPublishedBlogPosts.length; i++) {
-        let publishedBlogPostTitle = allPublishedBlogPosts[i].
-          getElementsByClassName('e2e-test-blog-post-title')[0].innerText;
-        if (publishedBlogPostTitle === blogPostTitle) {
-          count++;
-        }
-      }
-      if (count === 0) {
-        throw new Error(
-          `Blog post with title ${blogPostTitle} does not exist!`);
-      } else if (count > 1) {
-        throw new Error(
-          `Blog post with title ${blogPostTitle} exists more than once!`);
-      }
-    }, blogPostTitle);
-    showMessage(
-      `Published blog post with title ${blogPostTitle} exists!`);
-  }
-
-  /**
-   * This function checks if the blog dashboard is not accessible by the user.
-   */
-  async expectBlogDashboardAccessToBeUnauthorized() {
-    await this.goto(blogDashboardUrl);
-    try {
-      await this.page.waitForSelector(unauthErrorContainer);
-      showMessage('User unauthorized to access blog dashboard!');
-    } catch (err) {
-      throw new Error(
-        'No unauthorization error on accessing the blog dashboard page!');
-    }
-  }
-
-  /**
-   * This function checks if the blog dashboard is accessible by the user.
-   */
-  async expectBlogDashboardAccessToBeAuthorized() {
-    /** Here we are trying to check if the blog dashboard is accessible to the
-     * guest user after giving the blog admin role to it. There is a
-     * modal dialog box asking for the user name and bio for the users
-     * given blog admin role as they first time opens the blog-dashboard. */
-    await this.goto(blogDashboardUrl);
-    try {
-      await this.page.waitForSelector(blogDashboardAuthorDetailsModal);
-      showMessage('User authorized to access blog dashboard!');
-    } catch (err) {
-      throw new Error('User unauthorized to access blog dashboard!');
-    }
-  }
-
-  /**
-   * This function assigns a user with a role from the blog admin page.
-   * @param {string} username - The username of the user.
-   * @param {string} role - The role of the user.
-   */
-  async assignUserToRoleFromBlogAdminPage(username, role) {
-    await this.goto(blogAdminUrl);
-    await this.page.select('select#label-target-update-form-role-select', role);
-    await this.type(roleUpdateUsernameInput, username);
-    await this.clickOn('button.oppia-blog-admin-update-role-button');
-  }
-
-  /**
-   * This function removes blog editor role from the users.
-   * @param {string} username - The username of the user.
-   */
-  async removeBlogEditorRoleFromUsername(username) {
-    await this.goto(blogAdminUrl);
-    await this.type(blogEditorUsernameInput, username);
-    await this.clickOn('button.oppia-blog-admin-remove-blog-editor-button');
-  }
-
-  /**
-   * This function changes the blog tags limit.
-   * @param {number} limit - The limit of the blog tags.
-   */
-  async setMaximumTagLimitTo(limit) {
-    // These steps are for deleting the existing value in the input field.
-    const tagInputField = await this.page.$(maximumTagLimitInput);
-    await tagInputField.click({ clickCount: 3 });
-    await this.page.keyboard.press('Backspace');
-
-    await this.type(maximumTagLimitInput, limit);
-    await this.clickOn(LABEL_FOR_SAVE_BUTTON);
-
-    showMessage(`Successfully updated the tag limit to ${limit}!`);
-  }
-
-  /**
-   * This function checks if the tag limit is not equal to.
-   * @param {number} limit - The limit of the blog tags.
-   */
-  async expectMaximumTagLimitNotToBe(limit) {
-    await this.page.evaluate(async(limit) => {
-      const tagLimit = document.getElementById('float-input').value;
-      if (tagLimit.value === limit) {
-        throw new Error(`Maximum tag limit is already ${limit}!`);
-      }
-    }, limit);
-    showMessage(`Maximum tag limit is not ${limit}!`);
-  }
-
-  /**
-   * This function checks if the tag limit is equal to.
-   * @param {number} limit - The limit of the blog tags.
-   */
-  async expectMaximumTagLimitToBe(limit) {
-    await this.page.evaluate(async(limit) => {
-      const tagLimit = document.getElementById('float-input').value;
-      if (tagLimit.value !== limit) {
-        throw new Error(`Maximum tag limit is not ${limit}!`);
-      }
-    });
-    showMessage(`Maximum tag is currently ${limit}!`);
-  }
-};
diff --git a/core/tests/puppeteer-acceptance-tests/user-utilities/blog-post-editor-utils.ts b/core/tests/puppeteer-acceptance-tests/user-utilities/blog-post-editor-utils.ts
new file mode 100644
--- /dev/null
+++ b/core/tests/puppeteer-acceptance-tests/user-utilities/blog-post-editor-utils.ts
@@ -0,0 +1,393 @@
+// Copyright 2024 The Oppia Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Blog Admin users utility file.
+ */
+
+import { IBaseUser, BaseUser } from
+  '../puppeteer-testing-utilities/puppeteer-utils';
+import testConstants from
+  '../puppeteer-testing-utilities/test-constants';
+import { showMessage } from
+  '../puppeteer-testing-utilities/show-message-utils';
+
+const blogTitleInput = 'input.e2e-test-blog-post-title-field';
+const blogBodyInput = 'div.e2e-test-rte';
+const thumbnailPhotoBox = 'div.e2e-test-photo-clickable';
+const unauthErrorContainer = 'div.e2e-test-error-container';
+const blogDashboardAuthorDetailsModal = 'div.modal-dialog';
+const blogAuthorBioField = 'textarea.e2e-test-blog-author-bio-field';
+const blogDashboardUrl = testConstants.URLs.BlogDashboard;
+const publishBlogPostButton = 'button.e2e-test-publish-blog-post-button';
+const addThumbnailImageButton = 'button.e2e-test-photo-upload-submit';
+const blogPostThumbnailImage = testConstants.images.blogPostThumbnailImage;
+
+const LABEL_FOR_NEW_BLOG_POST_CREATE_BUTTON = 'CREATE NEW BLOG POST';
+const LABEL_FOR_SAVE_BUTTON = 'Save';
+const LABEL_FOR_DONE_BUTTON = 'DONE';
+const LABEL_FOR_SAVE_DRAFT_BUTTON = 'SAVE AS DRAFT';
+const LABEL_FOR_DELETE_BUTTON = 'Delete';
+const LABEL_FOR_CONFIRM_BUTTON = 'Confirm';
+
+export interface IBlogPostEditor extends IBaseUser {
+  addUserBioInBlogDashboard: () => Promise<void>;
+  navigateToBlogDashboardPage: () => Promise<void>;
+  createDraftBlogPostWithTitle:
+    (draftBlogPostTitle: string) => Promise<void>;
+  deleteDraftBlogPostWithTitle:
+    (draftBlogPostTitle: string) => Promise<void>;
+  expectPublishButtonToBeDisabled: () => Promise<void>;
+  publishNewBlogPostWithTitle:
+    (newBlogPostTitle: string) => Promise<void>;
+  createNewBlogPostWithTitle:
+    (newBlogPostTitle: string) => Promise<void>;
+  deletePublishedBlogPostWithTitle: (blogPostTitle: string) => Promise<void>;
+  expectUserUnableToPublishBlogPost:
+    (expectedWarningMessage: string) => Promise<void>;
+  expectNumberOfBlogPostsToBe: (number: number) => Promise<void>;
+  navigateToPublishTab: () => Promise<void>;
+  expectDraftBlogPostWithTitleToBePresent: (
+    checkDraftBlogPostByTitle: string) => Promise<void>;
+  expectPublishedBlogPostWithTitleToBePresent: (
+    blogPostTitle: string) => Promise<void>;
+  expectBlogDashboardAccessToBeUnauthorized: () => Promise<void>;
+  expectBlogDashboardAccessToBeAuthorized: () => Promise<void>;
+}
+
+class BlogPostEditor extends BaseUser implements IBlogPostEditor {
+  /**
+   * Function for adding blog post author bio in blog dashboard.
+   */
+  async addUserBioInBlogDashboard(): Promise<void> {
+    await this.type(blogAuthorBioField, 'Dummy-User-Bio');
+    await this.page.waitForSelector(
+      'button.e2e-test-save-author-details-button:not([disabled])');
+    await this.clickOn(LABEL_FOR_SAVE_BUTTON);
+  }
+
+  /**
+   * Function for navigating to the blog dashboard page.
+   */
+  async navigateToBlogDashboardPage(): Promise<void> {
+    await this.goto(blogDashboardUrl);
+  }
+
+  /**
+   * This function creates a blog post with given title.
+   * to be created.
+   */
+  async createDraftBlogPostWithTitle(
+      draftBlogPostTitle: string
+  ): Promise<void> {
+    await this.addUserBioInBlogDashboard();
+    /** Giving explicit timeout because we need to wait for small
+     * transition to complete. We cannot wait for the next element to click
+     * using its selector as it is instantly loaded in the DOM but cannot
+     * be clicked until the transition is completed.
+     */
+    await this.page.waitForTimeout(500);
+    await this.clickOn(LABEL_FOR_NEW_BLOG_POST_CREATE_BUTTON);
+    await this.type(blogTitleInput, draftBlogPostTitle);
+    await this.page.keyboard.press('Tab');
+    await this.type(blogBodyInput, 'test blog post body content');
+    await this.clickOn(LABEL_FOR_DONE_BUTTON);
+    await this.page.waitForSelector(
+      'button.e2e-test-save-as-draft-button:not([disabled])');
+    await this.clickOn(LABEL_FOR_SAVE_DRAFT_BUTTON);
+
+    showMessage('Successfully created a draft blog post!');
+    await this.goto(blogDashboardUrl);
+  }
+
+  /**
+   * This function deletes a draft blog post with given title.
+   */
+  async deleteDraftBlogPostWithTitle(
+      draftBlogPostTitle: string
+  ): Promise<void> {
+    const allDraftBlogPosts =
+      await this.page.$$('.blog-dashboard-tile-content');
+    for (let i = 0; i < allDraftBlogPosts.length; i++) {
+      let checkDraftBlogPostTitle = await allDraftBlogPosts[i].$eval(
+        '.e2e-test-blog-post-title',
+        element => (element as HTMLElement).innerText);
+      if (draftBlogPostTitle === checkDraftBlogPostTitle) {
+        await allDraftBlogPosts[i].$eval(
+          '.e2e-test-blog-post-edit-box',
+          element => (element as HTMLElement).click());
+        /** Giving explicit timeout because we need to wait for small
+         * transition to complete. We cannot wait for the next element to click
+         * using its selector as it is instantly loaded in the DOM but cannot
+         * be clicked until the transition is completed.
+         */
+        await this.page.waitForTimeout(100);
+        await this.clickOn(LABEL_FOR_DELETE_BUTTON);
+        await this.page.waitForSelector('div.modal-dialog');
+        await this.clickOn(LABEL_FOR_CONFIRM_BUTTON);
+        showMessage('Draft blog post with given title deleted successfully!');
+        return;
+      }
+    }
+  }
+
+  /**
+   * This function checks if the Publish button is disabled.
+   */
+  async expectPublishButtonToBeDisabled(): Promise<void> {
+    await this.page.waitForSelector(publishBlogPostButton);
+    const publishedButtonIsDisabled = await this.page.$eval(
+      publishBlogPostButton, button => (button as HTMLButtonElement).disabled);
+    if (!publishedButtonIsDisabled) {
+      throw new Error(
+        'Published button is not disabled when the blog post data is not' +
+        ' completely filled');
+    }
+    showMessage(
+      'Published button is disabled when blog post data is not completely' +
+      ' filled');
+  }
+
+  /**
+   * This function publishes a blog post with given title.
+   */
+  async publishNewBlogPostWithTitle(newBlogPostTitle: string): Promise<void> {
+    await this.addUserBioInBlogDashboard();
+    /** Giving explicit timeout because we need to wait for small
+     * transition to complete. We cannot wait for the next element to click
+     * using its selector as it is instantly loaded in the DOM but cannot
+     * be clicked until the transition is completed.
+     */
+    await this.page.waitForTimeout(500);
+    await this.clickOn(LABEL_FOR_NEW_BLOG_POST_CREATE_BUTTON);
+
+    await this.expectPublishButtonToBeDisabled();
+    await this.clickOn('button.mat-button-toggle-button');
+    await this.expectPublishButtonToBeDisabled();
+    await this.clickOn(thumbnailPhotoBox);
+    await this.uploadFile(blogPostThumbnailImage);
+    await this.page.waitForSelector(
+      `${addThumbnailImageButton}:not([disabled])`);
+    await this.clickOn(addThumbnailImageButton);
+    await this.page.waitForSelector('body.modal-open', {hidden: true});
+    await this.expectPublishButtonToBeDisabled();
+
+    await this.type(blogTitleInput, newBlogPostTitle);
+    await this.page.keyboard.press('Tab');
+    await this.type(blogBodyInput, 'test blog post body content');
+    await this.clickOn(LABEL_FOR_DONE_BUTTON);
+
+    await this.page.waitForSelector(
+      `${publishBlogPostButton}:not([disabled])`);
+    await this.clickOn('PUBLISH');
+    await this.page.waitForSelector('button.e2e-test-confirm-button');
+    await this.clickOn(LABEL_FOR_CONFIRM_BUTTON);
+    showMessage('Successfully published a blog post!');
+  }
+
+  /**
+   * This function creates a new blog post with the given title.
+   */
+  async createNewBlogPostWithTitle(newBlogPostTitle: string): Promise<void> {
+    await this.clickOn('NEW POST');
+    await this.clickOn('button.mat-button-toggle-button');
+    await this.clickOn(thumbnailPhotoBox);
+    await this.uploadFile(blogPostThumbnailImage);
+    await this.page.waitForSelector(
+      `${addThumbnailImageButton}:not([disabled])`);
+    await this.clickOn(addThumbnailImageButton);
+    await this.page.waitForSelector('body.modal-open', {hidden: true});
+
+    await this.type(blogTitleInput, newBlogPostTitle);
+    await this.page.keyboard.press('Tab');
+    await this.type(blogBodyInput, 'test blog post body content - duplicate');
+    await this.clickOn(LABEL_FOR_DONE_BUTTON);
+  }
+
+  /**
+   * This function deletes a published blog post with the given title.
+   */
+  async deletePublishedBlogPostWithTitle(blogPostTitle: string): Promise<void> {
+    await this.clickOn('PUBLISHED');
+    const allPublishedBlogPosts =
+      await this.page.$$('.blog-dashboard-tile-content');
+    for (let i = 0; i < allPublishedBlogPosts.length; i++) {
+      let publishedBlogPostTitle = await allPublishedBlogPosts[i].$eval(
+        '.e2e-test-blog-post-title',
+        element => (element as HTMLElement).innerText);
+      if (publishedBlogPostTitle === blogPostTitle) {
+        await allPublishedBlogPosts[i].$eval(
+          '.e2e-test-blog-post-edit-box',
+          element => (element as HTMLElement).click());
+        /** Giving explicit timeout because we need to wait for small
+         * transition to complete. We cannot wait for the next element to click
+         * using its selector as it is instantly loaded in the DOM but cannot
+         * be clicked until the transition is completed.
+         */
+        await this.page.waitForTimeout(100);
+        await this.clickOn(LABEL_FOR_DELETE_BUTTON);
+        await this.page.waitForSelector('button.e2e-test-confirm-button');
+        await this.clickOn(LABEL_FOR_CONFIRM_BUTTON);
+        showMessage(
+          'Published blog post with given title deleted successfully!');
+        return;
+      }
+    }
+  }
+
+  /**
+   * This function checks that the user is unable to publish a blog post.
+   */
+  async expectUserUnableToPublishBlogPost(
+      expectedWarningMessage: string
+  ): Promise<void> {
+    const toastMessageBox = await this.page.$(
+      'div.e2e-test-toast-warning-message');
+    const toastMessageWarning = await this.page.evaluate(
+      (element: HTMLDivElement) => element.textContent, toastMessageBox);
+    const isPublishButtonDisabled = await this.page.$eval(
+      publishBlogPostButton,
+      button => (button as HTMLButtonElement).disabled);
+
+    if (!isPublishButtonDisabled) {
+      throw new Error('User is able to publish the blog post');
+    }
+    if (expectedWarningMessage !== toastMessageWarning) {
+      throw new Error(
+        'Expected warning message is not same as the actual warning message\n' +
+        `Expected warning: ${expectedWarningMessage}\n` +
+        `Displayed warning: ${toastMessageWarning}\n`);
+    }
+
+    showMessage(
+      'User is unable to publish the blog post because ' + toastMessageWarning);
+  }
+
+  /**
+   * This function checks the number of the blog posts in the blog dashboard.
+   */
+  async expectNumberOfBlogPostsToBe(number: number): Promise<void> {
+    const allBlogPosts = await this.page.$$(
+      '.blog-dashboard-tile-content');
+    if (allBlogPosts.length !== number) {
+      throw new Error(
+        `Number of blog posts is not equal to ${number}`);
+    }
+
+    showMessage(`Number of blog posts is equal to ${number}`);
+  }
+
+  /**
+   * This function navigates to the Published tab in the blog-dashbaord.
+   */
+  async navigateToPublishTab(): Promise<void> {
+    await this.goto(blogDashboardUrl);
+    await this.clickOn('PUBLISHED');
+    showMessage('Navigated to publish tab.');
+  }
+
+  /**
+   * This function checks a draft blog post to be created with the given title.
+   */
+  async expectDraftBlogPostWithTitleToBePresent(
+      checkDraftBlogPostByTitle: string
+  ): Promise<void> {
+    await this.goto(blogDashboardUrl);
+    const allDraftBlogPosts =
+      await this.page.$$('.blog-dashboard-tile-content');
+    let count = 0;
+    for (let i = 0; i < allDraftBlogPosts.length; i++) {
+      let draftBlogPostTitle = await allDraftBlogPosts[i].$eval(
+        '.e2e-test-blog-post-title',
+        element => (element as HTMLElement).innerText);
+      if (draftBlogPostTitle === checkDraftBlogPostByTitle) {
+        count++;
+      }
+    }
+    if (count === 0) {
+      throw new Error(
+        `Draft blog post with title ${checkDraftBlogPostByTitle} does not` +
+        ' exist!');
+    } else if (count > 1) {
+      throw new Error(
+        `Draft blog post with title ${checkDraftBlogPostByTitle} exists` +
+        ' more than once!');
+    }
+    showMessage(
+      `Draft blog post with title ${checkDraftBlogPostByTitle} exists!`);
+  }
+
+  /**
+   * This function checks if the blog post with given title is published.
+   */
+  async expectPublishedBlogPostWithTitleToBePresent(
+      blogPostTitle: string
+  ): Promise<void> {
+    await this.goto(blogDashboardUrl);
+    await this.clickOn('PUBLISHED');
+    const allPublishedBlogPosts = await this.page.$$(
+      '.blog-dashboard-tile-content');
+    let count = 0;
+    for (let i = 0; i < allPublishedBlogPosts.length; i++) {
+      let publishedBlogPostTitle = await allPublishedBlogPosts[i].$eval(
+        '.e2e-test-blog-post-title',
+        element => (element as HTMLElement).innerText);
+      if (publishedBlogPostTitle === blogPostTitle) {
+        count++;
+      }
+    }
+    if (count === 0) {
+      throw new Error(
+        `Blog post with title ${blogPostTitle} does not exist!`);
+    } else if (count > 1) {
+      throw new Error(
+        `Blog post with title ${blogPostTitle} exists more than once!`);
+    }
+    showMessage(
+      `Published blog post with title ${blogPostTitle} exists!`);
+  }
+
+  /**
+   * This function checks if the blog dashboard is not accessible by the user.
+   */
+  async expectBlogDashboardAccessToBeUnauthorized(): Promise<void> {
+    await this.goto(blogDashboardUrl);
+    try {
+      await this.page.waitForSelector(unauthErrorContainer);
+      showMessage('User unauthorized to access blog dashboard!');
+    } catch (err) {
+      throw new Error(
+        'No unauthorization error on accessing the blog dashboard page!');
+    }
+  }
+
+  /**
+   * This function checks if the blog dashboard is accessible by the user.
+   */
+  async expectBlogDashboardAccessToBeAuthorized(): Promise<void> {
+    /** Here we are trying to check if the blog dashboard is accessible to the
+     * guest user after giving the blog admin role to it. There is a
+     * modal dialog box asking for the user name and bio for the users
+     * given blog admin role as they first time opens the blog-dashboard. */
+    await this.goto(blogDashboardUrl);
+    try {
+      await this.page.waitForSelector(blogDashboardAuthorDetailsModal);
+      showMessage('User authorized to access blog dashboard!');
+    } catch (err) {
+      throw new Error('User unauthorized to access blog dashboard!');
+    }
+  }
+}
+
+export let BlogPostEditorFactory = (): IBlogPostEditor => new BlogPostEditor();
diff --git a/core/tests/puppeteer-acceptance-tests/user-utilities/logged-in-users-utils.js b/core/tests/puppeteer-acceptance-tests/user-utilities/logged-in-users-utils.ts
similarity index 81%
rename from core/tests/puppeteer-acceptance-tests/user-utilities/logged-in-users-utils.js
rename to core/tests/puppeteer-acceptance-tests/user-utilities/logged-in-users-utils.ts
--- a/core/tests/puppeteer-acceptance-tests/user-utilities/logged-in-users-utils.js
+++ b/core/tests/puppeteer-acceptance-tests/user-utilities/logged-in-users-utils.ts
@@ -1,4 +1,4 @@
-// Copyright 2023 The Oppia Authors. All Rights Reserved.
+// Copyright 2024 The Oppia Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -16,12 +16,12 @@
  * @fileoverview Logged-in users utility file.
  */
 
-const baseUser = require(
-  '../puppeteer-testing-utilities/puppeteer-utils.js');
-const testConstants = require(
-  '../puppeteer-testing-utilities/test-constants.js');
-const { showMessage } = require(
-  '../puppeteer-testing-utilities/show-message-utils.js');
+import { IBaseUser, BaseUser } from
+  '../puppeteer-testing-utilities/puppeteer-utils';
+import testConstants from
+  '../puppeteer-testing-utilities/test-constants';
+import { showMessage } from
+  '../puppeteer-testing-utilities/show-message-utils';
 
 const homeUrl = testConstants.URLs.Home;
 const aboutUrl = testConstants.URLs.About;
@@ -89,32 +89,65 @@ const dismissButton = 'i.e2e-test-thanks-for-donating-page-dismiss-button';
 const thanksForDonatingClass = '.modal-open';
 const donatePage = '.modal-backdrop.fade';
 
-module.exports = class LoggedInUsers extends baseUser {
+export interface ILoggedInUser extends IBaseUser {
+  navigateToHome: () => Promise<void>;
+  navigateToAboutPage: () => Promise<void>;
+  navigateToAboutFoundationPage: () => Promise<void>;
+  navigateToThanksForDonatingPage: () => Promise<void>;
+  clickAboutButtonInAboutMenuOnNavbar: () => Promise<void>;
+  clickBrowseOurLessonsButtonInAboutPage: () => Promise<void>;
+  clickAccessAndroidAppButtonInAboutPage: () => Promise<void>;
+  clickVisitClassroomButtonInAboutPage: () => Promise<void>;
+  clickBrowseLibraryButtonInAboutPage: () => Promise<void>;
+  clickCreateLessonsButtonInAboutPage: () => Promise<void>;
+  clickExploreLessonsButtonInAboutPage: () => Promise<void>;
+  clickAboutFoundationButtonInAboutMenuOnNavbar: () => Promise<void>;
+  click61MillionChildrenLinkInAboutFoundation: () => Promise<void>;
+  clickEvenThoseWhoAreInSchoolLinkInAboutFoundation: () => Promise<void>;
+  clickSourceUnescoLinkInAboutFoundation: () => Promise<void>;
+  click420MillionLinkInAboutFoundation: () => Promise<void>;
+  clickLearnMoreAboutOppiaButtonInAboutFoundation: () => Promise<void>;
+  clickBecomeAVolunteerButtonInAboutFoundation: () => Promise<void>;
+  clickConsiderBecomingAPartnerTodayLinkInAboutFoundation: () => Promise<void>;
+  clickJoinOurLargeVolunteerCommunityLinkInAboutFoundation: () => Promise<void>;
+  clickDonationsLinkInAboutFoundation: () => Promise<void>;
+  clickBlogButtonInAboutMenuOnNavbar: () => Promise<void>;
+  clickPartnershipsButtonInGetInvolvedMenuOnNavbar: () => Promise<void>;
+  clickVolunteerButtonInGetInvolvedMenuOnNavbar: () => Promise<void>;
+  clickDonateButtonInGetInvolvedMenuOnNavbar: () => Promise<void>;
+  clickContactUsButtonInGetInvolvedMenuOnNavbar: () => Promise<void>;
+  clickDonateButtonOnNavbar: () => Promise<void>;
+  clickWatchAVideoButtonInThanksForDonatingPage: () => Promise<void>;
+  clickReadOurBlogButtonInThanksForDonatingPage: () => Promise<void>;
+  clickDismissButtonInThanksForDonatingPage: () => Promise<void>;
+}
+
+class LoggedInUser extends BaseUser implements ILoggedInUser {
   /**
    * Function to navigate to the home page.
    */
-  async navigateToHome() {
+  async navigateToHome(): Promise<void> {
     await this.goto(homeUrl);
   }
 
   /**
    * Function to navigate to the about page.
    */
-  async navigateToAboutPage() {
+  async navigateToAboutPage(): Promise<void> {
     await this.goto(aboutUrl);
   }
 
   /**
    * Function to navigate to the about foundation page.
    */
-  async navigateToAboutFoundationPage() {
+  async navigateToAboutFoundationPage(): Promise<void> {
     await this.goto(aboutFoundationUrl);
   }
 
   /**
    * Function to navigate to the Thanks for Donating page.
    */
-  async navigateToThanksForDonatingPage() {
+  async navigateToThanksForDonatingPage(): Promise<void> {
     await Promise.all([
       this.page.waitForNavigation(),
       this.page.goto(thanksForDonatingUrl),
@@ -123,15 +156,13 @@ module.exports = class LoggedInUsers extends baseUser {
 
   /**
    * Function to click a button and check if it opens the expected destination.
-   * @param {string} button - the CSS selector of the button element
-   * @param {string} buttonName - the name of the button
-   * @param {string} expectedDestinationPageUrl - the expected page that
-   *   the button should navigate to
-   * @param {string} expectedDestinationPageName - the name of the expected page
    */
   async clickButtonToNavigateToNewPage(
-      button, buttonName, expectedDestinationPageUrl,
-      expectedDestinationPageName) {
+      button: string,
+      buttonName: string,
+      expectedDestinationPageUrl: string,
+      expectedDestinationPageName: string
+  ): Promise<void> {
     await Promise.all([
       this.page.waitForNavigation(),
       await this.clickOn(button),
@@ -151,7 +182,7 @@ module.exports = class LoggedInUsers extends baseUser {
    * Function to click the About button in the About Menu on navbar
    * and check if it opens the About page.
    */
-  async clickAboutButtonInAboutMenuOnNavbar() {
+  async clickAboutButtonInAboutMenuOnNavbar(): Promise<void> {
     await this.clickOn(navbarAboutTab);
     await this.clickButtonToNavigateToNewPage(
       navbarAboutTabAboutButton,
@@ -162,7 +193,7 @@ module.exports = class LoggedInUsers extends baseUser {
    * Function to click the Browse Our Lessons button in the About page
    * and check if it opens the Math Classroom page.
    */
-  async clickBrowseOurLessonsButtonInAboutPage() {
+  async clickBrowseOurLessonsButtonInAboutPage(): Promise<void> {
     await this.clickButtonToNavigateToNewPage(
       browseOurLessonsButton,
       'Browse Our Lessons button', mathClassroomUrl, 'Math Classroom');
@@ -172,7 +203,7 @@ module.exports = class LoggedInUsers extends baseUser {
    * Function to click the Access Android App button in the About page
    * and check if it opens the Android page.
    */
-  async clickAccessAndroidAppButtonInAboutPage() {
+  async clickAccessAndroidAppButtonInAboutPage(): Promise<void> {
     await this.clickButtonToNavigateToNewPage(
       accessAndroidAppButton,
       'Access the Android App button', androidUrl, 'Android');
@@ -182,7 +213,7 @@ module.exports = class LoggedInUsers extends baseUser {
    * Function to click the Visit Classroom button in the About page
    * and check if it opens the Math Classroom page.
    */
-  async clickVisitClassroomButtonInAboutPage() {
+  async clickVisitClassroomButtonInAboutPage(): Promise<void> {
     await this.clickButtonToNavigateToNewPage(
       visitClassroomButton,
       'Visit Classroom button', mathClassroomUrl, 'Math Classroom');
@@ -192,7 +223,7 @@ module.exports = class LoggedInUsers extends baseUser {
    * Function to click the Browse Library button in the About page
    * and check if it opens the Community Library page.
    */
-  async clickBrowseLibraryButtonInAboutPage() {
+  async clickBrowseLibraryButtonInAboutPage(): Promise<void> {
     await this.clickButtonToNavigateToNewPage(
       browseLibraryButton,
       'Browse Library button', communityLibraryUrl, 'Community Library');
@@ -202,7 +233,7 @@ module.exports = class LoggedInUsers extends baseUser {
    * Function to click the Browse Our Lessons button in the About page
    * and check if it opens the Creator Dashboard page in Create Mode.
    */
-  async clickCreateLessonsButtonInAboutPage() {
+  async clickCreateLessonsButtonInAboutPage(): Promise<void> {
     await this.clickOn(createLessonsButton);
     if (this.page.url() !== creatorDashboardCreateModeUrl) {
       throw new Error (
@@ -230,7 +261,7 @@ module.exports = class LoggedInUsers extends baseUser {
    * Function to click the Browse Our Lessons button in the About page
    * and check if it opens the Math Classroom page.
    */
-  async clickExploreLessonsButtonInAboutPage() {
+  async clickExploreLessonsButtonInAboutPage(): Promise<void> {
     await this.clickButtonToNavigateToNewPage(
       exploreLessonsButton,
       'Explore Lessons button', mathClassroomUrl, 'Math Classroom');
@@ -240,13 +271,13 @@ module.exports = class LoggedInUsers extends baseUser {
    * Function to click the The Oppia Foundation button in the About Menu
    * on navbar and check if it opens The About Foundation page.
    */
-  async clickAboutFoundationButtonInAboutMenuOnNavbar() {
+  async clickAboutFoundationButtonInAboutMenuOnNavbar(): Promise<void> {
     await this.clickOn(navbarAboutTab);
     await this.clickOn(navbarAboutTabAboutFoundationButton);
     await this.page.waitForSelector(aboutFoundationClass);
     const displayedH1 = await this.page.$eval(
       aboutFoundationClass,
-      element => element.innerText
+      (element) => (element as HTMLElement).innerText
     );
     if ((this.page.url() !== aboutFoundationUrl) &&
       (displayedH1 !== 'THE OPPIA FOUNDATION')) {
@@ -264,7 +295,7 @@ module.exports = class LoggedInUsers extends baseUser {
    * Function to click the 61 million children link
    * in the About Foundation page and check if it opens the right page.
    */
-  async click61MillionChildrenLinkInAboutFoundation() {
+  async click61MillionChildrenLinkInAboutFoundation(): Promise<void> {
     await this.page.waitForSelector(millionsOfContentId);
     const buttonText = await this.page.$eval(
       millionsOfContentId,
@@ -292,7 +323,7 @@ module.exports = class LoggedInUsers extends baseUser {
    * Function to click the even those who are in school link
    * in the About Foundation page and check if it opens the right page.
    */
-  async clickEvenThoseWhoAreInSchoolLinkInAboutFoundation() {
+  async clickEvenThoseWhoAreInSchoolLinkInAboutFoundation(): Promise<void> {
     await this.page.waitForSelector(millionsOfContentId);
     const buttonText = await this.page.$eval(
       millionsOfContentId,
@@ -320,7 +351,7 @@ module.exports = class LoggedInUsers extends baseUser {
    * Function to click the Source: UNESCO link in the About Foundation page
    * and check if it opens the right page.
    */
-  async clickSourceUnescoLinkInAboutFoundation() {
+  async clickSourceUnescoLinkInAboutFoundation(): Promise<void> {
     await this.clickButtonToNavigateToNewPage(
       sourceUnescoButton,
       'Source: UNESCO link', sourceUnescoUrl, 'right');
@@ -330,7 +361,7 @@ module.exports = class LoggedInUsers extends baseUser {
    * Function to click the 420 million link
    * in the About Foundation page and check if it opens the right page.
    */
-  async click420MillionLinkInAboutFoundation() {
+  async click420MillionLinkInAboutFoundation(): Promise<void> {
     await this.page.waitForSelector(weCannotContentId);
     const buttonText = await this.page.$eval(
       weCannotContentId,
@@ -357,7 +388,7 @@ module.exports = class LoggedInUsers extends baseUser {
    * Function to click the Learn More About Oppia button
    * in the About Foundation page and check if it opens the About page.
    */
-  async clickLearnMoreAboutOppiaButtonInAboutFoundation() {
+  async clickLearnMoreAboutOppiaButtonInAboutFoundation(): Promise<void> {
     await this.clickOn(learnMoreAboutOppiaButton);
     const newTab = await this.browserObject.waitForTarget(
       target => target.url() === aboutUrl
@@ -375,7 +406,7 @@ module.exports = class LoggedInUsers extends baseUser {
    * Function to click the Become A Volunteer button
    * in the About Foundation page and check if it opens the Volunteer page.
    */
-  async clickBecomeAVolunteerButtonInAboutFoundation() {
+  async clickBecomeAVolunteerButtonInAboutFoundation(): Promise<void> {
     await this.clickOn(becomeAVolunteerButton);
     const newTab = await this.browserObject.waitForTarget(
       target => target.url() === volunteerUrl
@@ -393,7 +424,8 @@ module.exports = class LoggedInUsers extends baseUser {
    * Function to click the Consider Becoming A Partner Today! link
    * in the About Foundation page and check if it opens the Partnerships page.
    */
-  async clickConsiderBecomingAPartnerTodayLinkInAboutFoundation() {
+  async clickConsiderBecomingAPartnerTodayLinkInAboutFoundation():
+    Promise<void> {
     await this.page.waitForSelector(sectionSixPart1);
     const buttonText = await this.page.$eval(
       sectionSixPart1,
@@ -422,7 +454,8 @@ module.exports = class LoggedInUsers extends baseUser {
    * Function to click the Join our large volunteer community! link
    * in the About Foundation page and check if it opens the Volunteer page.
    */
-  async clickJoinOurLargeVolunteerCommunityLinkInAboutFoundation() {
+  async clickJoinOurLargeVolunteerCommunityLinkInAboutFoundation():
+    Promise<void> {
     await this.page.waitForSelector(sectionSixPart2);
     const buttonText = await this.page.$eval(
       sectionSixPart2,
@@ -451,7 +484,7 @@ module.exports = class LoggedInUsers extends baseUser {
    * Function to click the donations link
    * in the About Foundation page and check if it opens the Donate page.
    */
-  async clickDonationsLinkInAboutFoundation() {
+  async clickDonationsLinkInAboutFoundation(): Promise<void> {
     await this.page.waitForSelector(sectionSixPart3);
     const buttonText = await this.page.$eval(
       sectionSixPart3,
@@ -478,7 +511,7 @@ module.exports = class LoggedInUsers extends baseUser {
    * Function to click the Blog button in the About Menu on navbar
    * and check if it opens the Blog page.
    */
-  async clickBlogButtonInAboutMenuOnNavbar() {
+  async clickBlogButtonInAboutMenuOnNavbar(): Promise<void> {
     await this.clickOn(navbarAboutTab);
     await this.clickButtonToNavigateToNewPage(
       navbarAboutTabBlogButton,
@@ -489,7 +522,7 @@ module.exports = class LoggedInUsers extends baseUser {
    * Function to click the School and Organizations button in the
    * Get Involved Menu on navbar and check if it opens the Partnerships page.
    */
-  async clickPartnershipsButtonInGetInvolvedMenuOnNavbar() {
+  async clickPartnershipsButtonInGetInvolvedMenuOnNavbar(): Promise<void> {
     await this.clickOn(navbarGetInvolvedTab);
     await this.clickButtonToNavigateToNewPage(
       navbarGetInvolvedTabSchoolAndOrganizationsButton,
@@ -501,7 +534,7 @@ module.exports = class LoggedInUsers extends baseUser {
    * Function to click the Volunteer button in the Get Involved Menu
    * on navbar and check if it opens the Volunteer page.
    */
-  async clickVolunteerButtonInGetInvolvedMenuOnNavbar() {
+  async clickVolunteerButtonInGetInvolvedMenuOnNavbar(): Promise<void> {
     await this.clickOn(navbarGetInvolvedTab);
     await this.clickButtonToNavigateToNewPage(
       navbarGetInvolvedTabVolunteerButton,
@@ -513,7 +546,7 @@ module.exports = class LoggedInUsers extends baseUser {
    * Function to click the Donate button in the Get Involved Menu
    * on navbar and check if it opens the Donate page.
    */
-  async clickDonateButtonInGetInvolvedMenuOnNavbar() {
+  async clickDonateButtonInGetInvolvedMenuOnNavbar(): Promise<void> {
     await this.clickOn(navbarGetInvolvedTab);
     await this.clickButtonToNavigateToNewPage(
       navbarGetInvolvedTabDonateButton,
@@ -524,7 +557,7 @@ module.exports = class LoggedInUsers extends baseUser {
    * Function to click the Contact Us button in the Get Involved Menu
    * on navbar and check if it opens the Partnerships page.
    */
-  async clickContactUsButtonInGetInvolvedMenuOnNavbar() {
+  async clickContactUsButtonInGetInvolvedMenuOnNavbar(): Promise<void> {
     await this.clickOn(navbarGetInvolvedTab);
     await this.clickButtonToNavigateToNewPage(
       navbarGetInvolvedTabContactUsButton,
@@ -536,7 +569,7 @@ module.exports = class LoggedInUsers extends baseUser {
    * Function to click the Donate button on navbar
    * and check if it opens the Donate page.
    */
-  async clickDonateButtonOnNavbar() {
+  async clickDonateButtonOnNavbar(): Promise<void> {
     await this.clickButtonToNavigateToNewPage(
       navbarDonateButton, 'Donate button on navbar', donateUrl, 'Donate');
   }
@@ -545,11 +578,11 @@ module.exports = class LoggedInUsers extends baseUser {
    * Function to click the Watch A Video button
    * in the Thanks for Donating page and check if it opens the right page.
    */
-  async clickWatchAVideoButtonInThanksForDonatingPage() {
+  async clickWatchAVideoButtonInThanksForDonatingPage(): Promise<void> {
     await this.page.waitForSelector(watchAVideoButton);
     const buttonText = await this.page.$eval(
       watchAVideoButton,
-      element => element.innerText
+      element => (element as HTMLElement).innerText
     );
     if (buttonText !== 'Watch a video') {
       throw new Error (
@@ -572,11 +605,11 @@ module.exports = class LoggedInUsers extends baseUser {
    * Function to click the Read Our Blog button
    * in the Thanks for Donating page and check if it opens the Blog page.
    */
-  async clickReadOurBlogButtonInThanksForDonatingPage() {
+  async clickReadOurBlogButtonInThanksForDonatingPage(): Promise<void> {
     await this.page.waitForSelector(readOurBlogButton);
     const buttonText = await this.page.$eval(
       readOurBlogButton,
-      element => element.innerText
+      element => (element as HTMLElement).innerText
     );
     if (buttonText !== 'Read our blog') {
       throw new Error (
@@ -600,7 +633,7 @@ module.exports = class LoggedInUsers extends baseUser {
    * and check if the Thanks for Donating popup disappears
    * and if the Donate page is shown
    */
-  async clickDismissButtonInThanksForDonatingPage() {
+  async clickDismissButtonInThanksForDonatingPage(): Promise<void> {
     await this.clickOn(dismissButton);
     const thanksForDonatingHeader = await this.page.$(thanksForDonatingClass);
     if (thanksForDonatingHeader !== null) {
@@ -617,4 +650,6 @@ module.exports = class LoggedInUsers extends baseUser {
         'and if the Donate page is shown.');
     }
   }
-};
+}
+
+export let LoggedInUserFactory = (): ILoggedInUser => new LoggedInUser();
diff --git a/core/tests/puppeteer-acceptance-tests/user-utilities/question-admin-utils.js b/core/tests/puppeteer-acceptance-tests/user-utilities/question-admin-utils.ts
similarity index 68%
rename from core/tests/puppeteer-acceptance-tests/user-utilities/question-admin-utils.js
rename to core/tests/puppeteer-acceptance-tests/user-utilities/question-admin-utils.ts
--- a/core/tests/puppeteer-acceptance-tests/user-utilities/question-admin-utils.js
+++ b/core/tests/puppeteer-acceptance-tests/user-utilities/question-admin-utils.ts
@@ -16,14 +16,14 @@
  * @fileoverview Question admin users utility file.
  */
 
-const baseUser = require(
-  '../puppeteer-testing-utilities/puppeteer-utils.js');
-const testConstants = require(
-  '../puppeteer-testing-utilities/test-constants.js');
+import { IBaseUser, BaseUser } from
+  '../puppeteer-testing-utilities/puppeteer-utils';
+import testConstants from
+  '../puppeteer-testing-utilities/test-constants';
 const contributorDashboardAdminUrl =
   testConstants.URLs.ContributorDashboardAdmin;
-const { showMessage } = require(
-  '../puppeteer-testing-utilities/show-message-utils.js');
+import { showMessage } from
+  '../puppeteer-testing-utilities/show-message-utils';
 
 const reviewQuestionRightValue = 'question';
 const submitQuestionRightValue = 'submit_question';
@@ -63,19 +63,38 @@ const removeContributonRightsCategorySelector =
 const removeContributionRightsSubmitButton =
   'button#remove-contribution-rights-submit-button';
 
-module.exports = class QuestionAdmin extends baseUser {
+export interface IQuestionAdmin extends IBaseUser {
+  navigateToContributorDashboardAdminPage: () => Promise<void>;
+  addReviewQuestionRights: (username: string) => Promise<void>;
+  addSubmitQuestionRights: (username: string) => Promise<void>;
+  removeReviewQuestionRights: (username: string) => Promise<void>;
+  removeSubmitQuestionRights: (username: string) => Promise<void>;
+  getDisplayedListOfQuestionReviewers: () => Promise<string[]>;
+  getDisplayedListOfQuestionSubmitters: () => Promise<string[]>;
+  getContributionStatusForUser: (
+    username: string, contribution: string) => Promise<string>;
+  verifyUserCanReviewQuestions: (username: string) => Promise<void>;
+  verifyUserCanSubmitQuestions: (username: string) => Promise<void>;
+  verifyUserCannotReviewQuestions: (username: string) => Promise<void>;
+  verifyUserCannotSubmitQuestions: (username: string) => Promise<void>;
+  verifyQuestionReviewersIncludeUser: (username: string) => Promise<void>;
+  verifyQuestionSubmittersIncludeUser: (username: string) => Promise<void>;
+  verifyQuestionReviewersExcludeUser: (username: string) => Promise<void>;
+  verifyQuestionSubmittersExcludeUser: (username: string) => Promise<void>;
+}
+
+class QuestionAdmin extends BaseUser implements IQuestionAdmin {
   /**
    * Function for navigating to the contributor dashboard admin page.
    */
-  async navigateToContributorDashboardAdminPage() {
+  async navigateToContributorDashboardAdminPage(): Promise<void> {
     await this.goto(contributorDashboardAdminUrl);
   }
 
   /**
    * Function for adding a right of reviewing questions to a user.
-   * @param {string} username - the username of the user.
    */
-  async addReviewQuestionRights(username) {
+  async addReviewQuestionRights(username: string): Promise<void> {
     await this.type(addContributorUsernameInput, username);
     await this.select(
       addContributonRightsCategorySelector, reviewQuestionRightValue);
@@ -86,9 +105,8 @@ module.exports = class QuestionAdmin extends baseUser {
 
   /**
    * Function for adding a right of submitting questions to a user.
-   * @param {string} username - the username of the user.
    */
-  async addSubmitQuestionRights(username) {
+  async addSubmitQuestionRights(username: string): Promise<void> {
     await this.type(addContributorUsernameInput, username);
     await this.select(
       addContributonRightsCategorySelector, submitQuestionRightValue);
@@ -99,9 +117,8 @@ module.exports = class QuestionAdmin extends baseUser {
 
   /**
    * Function for removng a right of reviewing questions to a user.
-   * @param {string} username - the username of the user.
    */
-  async removeReviewQuestionRights(username) {
+  async removeReviewQuestionRights(username: string): Promise<void> {
     await this.type(removeContributorUsernameInput, username);
     await this.select(
       removeContributonRightsCategorySelector, reviewQuestionRightValue);
@@ -112,9 +129,8 @@ module.exports = class QuestionAdmin extends baseUser {
 
   /**
    * Function for removing a right of reviewing questions to a user.
-   * @param {string} username - the username of the user.
    */
-  async removeSubmitQuestionRights(username) {
+  async removeSubmitQuestionRights(username: string): Promise<void> {
     await this.type(removeContributorUsernameInput, username);
     await this.select(
       removeContributonRightsCategorySelector, submitQuestionRightValue);
@@ -125,9 +141,8 @@ module.exports = class QuestionAdmin extends baseUser {
 
   /**
    * Function to return the list of question reviewers
-   * @returns {string[]} displayedUsers - list of strings of all username
    */
-  async getDisplayedListOfQuestionReviewers() {
+  async getDisplayedListOfQuestionReviewers(): Promise<string[]> {
     await this.select(viewContributorFilterMethodSelector, roleMethodValue);
     await this.select(
       viewContributorCategorySelector, reviewQuestionRightValue);
@@ -136,18 +151,20 @@ module.exports = class QuestionAdmin extends baseUser {
     await this.page.waitForNetworkIdle();
 
     await this.page.waitForSelector(viewRoleUserResult);
-    const displayedUsers = await this.page.$eval(
+    const displayedUsersText = await this.page.$eval(
       viewRoleUserResult,
-      element => element.innerText
+      element => (element as HTMLElement).innerText
     );
+    const displayedUsers =
+      displayedUsersText.replace('Usernames:', '').trim().replace(
+        '[', '').replace(']', '').split(',');
     return displayedUsers;
   }
 
   /**
    * Function to return the list of question reviewers
-   * @returns {string[]} displayedUsers - list of strings of all username
    */
-  async getDisplayedListOfQuestionSubmitters() {
+  async getDisplayedListOfQuestionSubmitters(): Promise<string[]> {
     await this.select(viewContributorFilterMethodSelector, roleMethodValue);
     await this.select(
       viewContributorCategorySelector, submitQuestionRightValue);
@@ -156,21 +173,23 @@ module.exports = class QuestionAdmin extends baseUser {
     await this.page.waitForNetworkIdle();
 
     await this.page.waitForSelector(viewRoleUserResult);
-    const displayedUsers = await this.page.$eval(
+    const displayedUsersText = await this.page.$eval(
       viewRoleUserResult,
-      element => element.innerText
+      element => (element as HTMLElement).innerText
     );
+    const displayedUsers =
+      displayedUsersText.replace('Usernames:', '').trim().replace(
+        '[', '').replace(']', '').split(',');
     return displayedUsers;
   }
 
   /**
    * Function to return the contribution rights status for the user.
-   * @param {string} username - the username of the user.
-   * @param {string} contribution - the css element of
-   * the result of contribution status to check
-   * @returns {string} contributionStatusForUser - the string of the result
    */
-  async getContributionStatusForUser(username, contribution) {
+  async getContributionStatusForUser(
+      username: string,
+      contribution: string
+  ): Promise<string> {
     await this.select(viewContributorFilterMethodSelector, usernameMethodValue);
     await this.type(viewContributerUsernameInput, username);
     await this.clickOn(viewContributorSubmitButton);
@@ -180,17 +199,16 @@ module.exports = class QuestionAdmin extends baseUser {
     await this.page.waitForSelector(contribution);
     const contributionStatusForUser = await this.page.$eval(
       contribution,
-      element => element.innerText);
+      element => (element as HTMLElement).innerText
+    );
     return contributionStatusForUser;
   }
 
   /**
    * Function to check if the user has the right to review questions
-   * @param {string} username - the username of the user to view.
    */
-  async verifyUserCanReviewQuestions(username) {
-    const questionReviewStatusForUser = await
-    this.getContributionStatusForUser(
+  async verifyUserCanReviewQuestions(username: string): Promise<void> {
+    const questionReviewStatusForUser = await this.getContributionStatusForUser(
       username, viewContributorReviewQuestionsResult);
 
     if (questionReviewStatusForUser === 'Not-allowed') {
@@ -204,11 +222,9 @@ module.exports = class QuestionAdmin extends baseUser {
 
   /**
    * Function to check if the user has the right to submit questions
-   * @param {string} username - the username of the user to view.
    */
-  async verifyUserCanSubmitQuestions(username) {
-    const questionSubmitStatusForUser = await
-    this.getContributionStatusForUser(
+  async verifyUserCanSubmitQuestions(username: string): Promise<void> {
+    const questionSubmitStatusForUser = await this.getContributionStatusForUser(
       username, viewContributorSubmitQuestionResult);
 
     if (questionSubmitStatusForUser === 'Not-allowed') {
@@ -222,11 +238,9 @@ module.exports = class QuestionAdmin extends baseUser {
 
   /**
    * Function to check if the user doesn't have the right to review questions
-   * @param {string} username - the username of the user to view.
    */
-  async verifyUserCannotReviewQuestions(username) {
-    const questionReviewStatusForUser = await
-    this.getContributionStatusForUser(
+  async verifyUserCannotReviewQuestions(username: string): Promise<void> {
+    const questionReviewStatusForUser = await this.getContributionStatusForUser(
       username, viewContributorReviewQuestionsResult);
 
     if (questionReviewStatusForUser === 'Allowed') {
@@ -240,11 +254,9 @@ module.exports = class QuestionAdmin extends baseUser {
 
   /**
    * Function to check if the user doesn't have the right to submit questions
-   * @param {string} username - the username of the user to view.
    */
-  async verifyUserCannotSubmitQuestions(username) {
-    const questionSubmitStatusForUser = await
-    this.getContributionStatusForUser(
+  async verifyUserCannotSubmitQuestions(username: string): Promise<void> {
+    const questionSubmitStatusForUser = await this.getContributionStatusForUser(
       username, viewContributorSubmitQuestionResult);
 
     if (questionSubmitStatusForUser === 'Allowed') {
@@ -258,9 +270,8 @@ module.exports = class QuestionAdmin extends baseUser {
 
   /**
    * Function to check if the user is displayed as a question reviewer
-   * @param {string} username - the user expected to be displayed.
    */
-  async verifyQuestionReviewersIncludeUser(username) {
+  async verifyQuestionReviewersIncludeUser(username: string): Promise<void> {
     const displayedUsers = await this.getDisplayedListOfQuestionReviewers();
 
     if (!displayedUsers.includes(username)) {
@@ -271,9 +282,8 @@ module.exports = class QuestionAdmin extends baseUser {
 
   /**
    * Function to check if the user is displayed as a question submitter
-   * @param {string} username - the user expected to be displayed.
    */
-  async verifyQuestionSubmittersIncludeUser(username) {
+  async verifyQuestionSubmittersIncludeUser(username: string): Promise<void> {
     const displayedUsers = await this.getDisplayedListOfQuestionSubmitters();
 
     if (!displayedUsers.includes(username)) {
@@ -284,9 +294,8 @@ module.exports = class QuestionAdmin extends baseUser {
 
   /**
    * Function check if the user is not displayed as a question reviewer
-   * @param {string} username - the user expected to not be displayed.
    */
-  async verifyQuestionReviewersExcludeUser(username) {
+  async verifyQuestionReviewersExcludeUser(username: string): Promise<void> {
     const displayedUsers = await this.getDisplayedListOfQuestionReviewers();
 
     if (displayedUsers.includes(username)) {
@@ -297,9 +306,8 @@ module.exports = class QuestionAdmin extends baseUser {
 
   /**
    * Function to check if the user is not displayed as a question submitter
-   * @param {string} username - the user expected to not be displayed.
    */
-  async verifyQuestionSubmittersExcludeUser(username) {
+  async verifyQuestionSubmittersExcludeUser(username: string): Promise<void> {
     const displayedUsers = await this.getDisplayedListOfQuestionSubmitters();
 
     if (displayedUsers.includes(username)) {
@@ -307,4 +315,6 @@ module.exports = class QuestionAdmin extends baseUser {
         `${username} has the right to submit questions!`);
     }
   }
-};
+}
+
+export let QuestionAdminFactory = (): IQuestionAdmin => new QuestionAdmin();
diff --git a/core/tests/puppeteer-acceptance-tests/user-utilities/super-admin-utils.js b/core/tests/puppeteer-acceptance-tests/user-utilities/super-admin-utils.js
deleted file mode 100644
--- a/core/tests/puppeteer-acceptance-tests/user-utilities/super-admin-utils.js
+++ /dev/null
@@ -1,104 +0,0 @@
-// Copyright 2023 The Oppia Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS-IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-/**
- * @fileoverview Super Admin users utility file.
- */
-
-const baseUser = require(
-  '../puppeteer-testing-utilities/puppeteer-utils.js');
-const testConstants = require(
-  '../puppeteer-testing-utilities/test-constants.js');
-const { showMessage } = require(
-  '../puppeteer-testing-utilities/show-message-utils.js');
-
-const AdminPageRolesTab = testConstants.URLs.AdminPageRolesTab;
-const roleEditorInputField = 'input.e2e-test-username-for-role-editor';
-const roleEditorButtonSelector = 'button.e2e-test-role-edit-button';
-const rolesSelectDropdown = 'div.mat-select-trigger';
-const addRoleButton = 'button.oppia-add-role-button';
-
-module.exports = class e2eSuperAdmin extends baseUser {
-  /**
-   * The function to assign a role to a user.
-   * @param {string} username - The username to which role would be assigned.
-   * @param {string} role - The role that would be assigned to the user.
-   */
-  async assignRoleToUser(username, role) {
-    await this.goto(AdminPageRolesTab);
-    await this.type(roleEditorInputField, username);
-    await this.clickOn(roleEditorButtonSelector);
-    await this.clickOn(addRoleButton);
-    await this.clickOn(rolesSelectDropdown);
-    await this.page.evaluate(async(role) => {
-      const allRoles = document.getElementsByClassName('mat-option-text');
-      for (let i = 0; i < allRoles.length; i++) {
-        if (allRoles[i].innerText.toLowerCase() === role) {
-          allRoles[i].click({waitUntil: 'networkidle0'});
-          return;
-        }
-      }
-      throw new Error(`Role ${role} does not exists.`);
-    }, role);
-  }
-
-  /**
-   * The function excepts the user to have the given role.
-   * @param {string} username - The username to which role must be assigned.
-   * @param {string} role - The role which must be assigned to the user.
-   */
-  async expectUserToHaveRole(username, role) {
-    const currentPageUrl = this.page.url();
-    await this.goto(AdminPageRolesTab);
-    await this.type(roleEditorInputField, username);
-    await this.clickOn(roleEditorButtonSelector);
-    await this.page.waitForSelector('div.justify-content-between');
-    await this.page.evaluate((role) => {
-      const userRoles = document.getElementsByClassName(
-        'oppia-user-role-description');
-      for (let i = 0; i < userRoles.length; i++) {
-        if (userRoles[i].innerText.toLowerCase() === role) {
-          return;
-        }
-      }
-      throw new Error(`User does not have the ${role} role!`);
-    }, role);
-    showMessage(`User ${username} has the ${role} role!`);
-    await this.goto(currentPageUrl);
-  }
-
-  /**
-   * The function excepts the user to not have the given role.
-   * @param {string} username - The user to which the role must not be assigned.
-   * @param {string} role - The role which must not be assigned to the user.
-   */
-  async expectUserNotToHaveRole(username, role) {
-    const currentPageUrl = this.page.url();
-    await this.goto(AdminPageRolesTab);
-    await this.type(roleEditorInputField, username);
-    await this.clickOn(roleEditorButtonSelector);
-    await this.page.waitForSelector('div.justify-content-between');
-    await this.page.evaluate((role) => {
-      const userRoles = document.getElementsByClassName(
-        'oppia-user-role-description');
-      for (let i = 0; i < userRoles.length; i++) {
-        if (userRoles[i].innerText.toLowerCase() === role) {
-          throw new Error(`User has the ${role} role!`);
-        }
-      }
-    }, role);
-    showMessage(`User ${username} does not have the ${role} role!`);
-    await this.goto(currentPageUrl);
-  }
-};
diff --git a/core/tests/puppeteer-acceptance-tests/user-utilities/super-admin-utils.ts b/core/tests/puppeteer-acceptance-tests/user-utilities/super-admin-utils.ts
new file mode 100644
--- /dev/null
+++ b/core/tests/puppeteer-acceptance-tests/user-utilities/super-admin-utils.ts
@@ -0,0 +1,105 @@
+// Copyright 2024 The Oppia Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Super Admin users utility file.
+ */
+
+import { IBaseUser, BaseUser } from
+  '../puppeteer-testing-utilities/puppeteer-utils';
+import testConstants from
+  '../puppeteer-testing-utilities/test-constants';
+import { showMessage } from
+  '../puppeteer-testing-utilities/show-message-utils';
+
+const AdminPageRolesTab = testConstants.URLs.AdminPageRolesTab;
+const roleEditorInputField = 'input.e2e-test-username-for-role-editor';
+const roleEditorButtonSelector = 'button.e2e-test-role-edit-button';
+const rolesSelectDropdown = 'div.mat-select-trigger';
+const addRoleButton = 'button.oppia-add-role-button';
+
+export interface ISuperAdmin extends IBaseUser {
+  assignRoleToUser: (username: string, role: string) => Promise<void>;
+  expectUserToHaveRole: (username: string, role: string) => Promise<void>;
+  expectUserNotToHaveRole: (username: string, role: string) => Promise<void>;
+}
+
+class SuperAdmin extends BaseUser implements ISuperAdmin {
+  /**
+   * The function to assign a role to a user.
+   */
+  async assignRoleToUser(username: string, role: string): Promise<void> {
+    await this.goto(AdminPageRolesTab);
+    await this.type(roleEditorInputField, username);
+    await this.clickOn(roleEditorButtonSelector);
+    await this.clickOn(addRoleButton);
+    await this.clickOn(rolesSelectDropdown);
+    const allRoles = await this.page.$$('.mat-option-text');
+    for (let i = 0; i < allRoles.length; i++) {
+      const roleText = await this.page.evaluate(
+        (role: HTMLElement) => role.innerText, allRoles[i]);
+      if (roleText.toLowerCase() === role) {
+        await allRoles[i].click();
+        await this.page.waitForNetworkIdle();
+        return;
+      }
+    }
+    throw new Error(`Role ${role} does not exists.`);
+  }
+
+  /**
+   * The function expects the user to have the given role.
+   */
+  async expectUserToHaveRole(username: string, role: string): Promise<void> {
+    const currentPageUrl = this.page.url();
+    await this.goto(AdminPageRolesTab);
+    await this.type(roleEditorInputField, username);
+    await this.clickOn(roleEditorButtonSelector);
+    await this.page.waitForSelector('div.justify-content-between');
+    const userRoles = await this.page.$$('.oppia-user-role-description');
+    for (let i = 0; i < userRoles.length; i++) {
+      const roleText = await this.page.evaluate(
+        (role: HTMLElement) => role.innerText, userRoles[i]);
+      if (roleText.toLowerCase() === role) {
+        showMessage(`User ${username} has the ${role} role!`);
+        await this.goto(currentPageUrl);
+        return;
+      }
+    }
+    throw new Error(`User does not have the ${role} role!`);
+  }
+
+  /**
+   * The function expects the user to not have the given role.
+   */
+  async expectUserNotToHaveRole(username: string, role: string): Promise<void> {
+    const currentPageUrl = this.page.url();
+    await this.goto(AdminPageRolesTab);
+    await this.type(roleEditorInputField, username);
+    await this.clickOn(roleEditorButtonSelector);
+    await this.page.waitForSelector('div.justify-content-between');
+    const userRoles = await this.page.$$('.oppia-user-role-description');
+    for (let i = 0; i < userRoles.length; i++) {
+      const roleText = await this.page.evaluate(
+        (role: HTMLElement) => role.innerText, userRoles[i]);
+      if (roleText.toLowerCase() === role) {
+        throw new Error(`User has the ${role} role!`);
+      }
+    }
+    showMessage(`User ${username} does not have the ${role} role!`);
+    await this.goto(currentPageUrl);
+  }
+}
+
+export let SuperAdminFactory = (): ISuperAdmin => new SuperAdmin();
diff --git a/core/tests/puppeteer-acceptance-tests/user-utilities/translation-admin-utils.js b/core/tests/puppeteer-acceptance-tests/user-utilities/translation-admin-utils.ts
similarity index 69%
rename from core/tests/puppeteer-acceptance-tests/user-utilities/translation-admin-utils.js
rename to core/tests/puppeteer-acceptance-tests/user-utilities/translation-admin-utils.ts
--- a/core/tests/puppeteer-acceptance-tests/user-utilities/translation-admin-utils.js
+++ b/core/tests/puppeteer-acceptance-tests/user-utilities/translation-admin-utils.ts
@@ -1,4 +1,4 @@
-// Copyright 2023 The Oppia Authors. All Rights Reserved.
+// Copyright 2024 The Oppia Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -13,17 +13,17 @@
 // limitations under the License.
 
 /**
- * @fileoverview Translation admin users utility file.
+ * @fileoverview Translation admin role utility file.
  */
 
-const baseUser = require(
-  '../puppeteer-testing-utilities/puppeteer-utils.js');
-const testConstants = require(
-  '../puppeteer-testing-utilities/test-constants.js');
+import { IBaseUser, BaseUser } from
+  '../puppeteer-testing-utilities/puppeteer-utils';
+import testConstants from
+  '../puppeteer-testing-utilities/test-constants';
 const ContributorDashboardAdminUrl =
   testConstants.URLs.ContributorDashboardAdmin;
-const { showMessage } = require(
-  '../puppeteer-testing-utilities/show-message-utils.js');
+import { showMessage } from
+  '../puppeteer-testing-utilities/show-message-utils';
 
 const translationRightValue = 'translation';
 const usernameMethodValue = 'username';
@@ -65,21 +65,36 @@ const removeContributonRightsLanguageSelect =
 const removeContributionRightsSubmitButton =
   'button#remove-contribution-rights-submit-button';
 
-module.exports = class TranslationAdmin extends baseUser {
+export interface ITranslationAdmin extends IBaseUser {
+  navigateToContributorDashboardAdminPage: () => Promise<void>;
+  addTranslationLanguageReviewRights: (
+    username: string, languageCode: string) => Promise<void>;
+  removeTranslationLanguageReviewRights: (
+    username: string, languageCode: string) => Promise<void>;
+  viewContributionRightsForUser: (username: string) => Promise<void>;
+  viewContributorTranslationRightsByLanguageCode: (
+    languageCode: string) => Promise<void>;
+  expectDisplayedLanguagesToContain: (language: string) => Promise<void>;
+  expectUserToBeDisplayed: (username: string) => Promise<void>;
+  expectUserToNotBeDisplayed: (username: string) => Promise<void>;
+}
+
+class TranslationAdmin extends BaseUser implements ITranslationAdmin {
   /**
    * Function for navigating to the contributor dashboard admin page.
    */
-  async navigateToContributorDashboardAdminPage() {
+  async navigateToContributorDashboardAdminPage(): Promise<void> {
     await this.goto(ContributorDashboardAdminUrl);
   }
 
   /**
    * Function for adding a translation right to a user.
-   * @param {string} username - the username of the user.
-   * @param {string} languageCode - the language code of the language to add.
    */
 
-  async addTranslationLanguageReviewRights(username, languageCode) {
+  async addTranslationLanguageReviewRights(
+      username: string,
+      languageCode: string
+  ): Promise<void> {
     await this.type(addContributorUsernameInput, username);
     await this.select(
       addContributonRightsCategorySelect, translationRightValue);
@@ -92,10 +107,11 @@ module.exports = class TranslationAdmin extends baseUser {
 
   /**
    * Function for removing a translation right from a user.
-   * @param {string} username - the username of the user.
-   * @param {string} languageCode - the language code of the language to remove.
    */
-  async removeTranslationLanguageReviewRights(username, languageCode) {
+  async removeTranslationLanguageReviewRights(
+      username: string,
+      languageCode: string
+  ): Promise<void> {
     await this.type(removeContributorUsernameInput, username);
     await this.select(
       removeContributonRightsCategorySelect, translationRightValue);
@@ -108,9 +124,8 @@ module.exports = class TranslationAdmin extends baseUser {
 
   /**
    * Function to display contribution rights by user.
-   * @param {string} username - the username of the user to view.
    */
-  async viewContributionRightsForUser(username) {
+  async viewContributionRightsForUser(username: string): Promise<void> {
     await this.select(viewContributorFilterMethodSelect, usernameMethodValue);
     await this.type(viewContributerUsernameInput, username);
     await this.clickOn(viewContributorSubmitButton);
@@ -120,9 +135,10 @@ module.exports = class TranslationAdmin extends baseUser {
 
   /**
    * Function to display translation rights by language.
-   * @param {string} languageCode - the language option as an option value.
    */
-  async viewContributorTranslationRightsByLanguageCode(languageCode) {
+  async viewContributorTranslationRightsByLanguageCode(
+      languageCode: string
+  ): Promise<void> {
     await this.select(viewContributorFilterMethodSelect, roleMethodValue);
     await this.select(viewContributorCategorySelect, translationRightValue);
     await this.select(viewContributorLanguageSelect, languageCode);
@@ -133,13 +149,12 @@ module.exports = class TranslationAdmin extends baseUser {
 
   /**
    * Function to check if the language is displayed as a translation right.
-   * @param {string} language - the language to match.
    */
-  async expectDisplayedLanguagesToContain(language) {
+  async expectDisplayedLanguagesToContain(language: string): Promise<void> {
     await this.page.waitForSelector(viewContributorLanguageResult);
     const displayedLanguage = await this.page.$eval(
       viewContributorLanguageResult,
-      element => element.innerText);
+      element => (element as HTMLElement).innerText);
     if (!displayedLanguage.includes(language)) {
       throw new Error(
         `Selected user does not have translation rights for ${language}!`);
@@ -151,33 +166,34 @@ module.exports = class TranslationAdmin extends baseUser {
 
   /**
    * Function to check if the user is displayed as a translator.
-   * @param {string} usenamer - the user expected to be displayed.
    */
-  async expectUserToBeDisplayed(username) {
+  async expectUserToBeDisplayed(username: string): Promise<void> {
     await this.page.waitForSelector(viewLanguageRoleUserResult);
     const displayedUsers = await this.page.$eval(
       viewLanguageRoleUserResult,
-      element => element.innerText
+      element => (element as HTMLElement).innerText
     );
     if (!displayedUsers.includes(username)) {
       throw new Error(
-        `${user} does not have translation rights for selected language!`);
+        `${username} does not have translation rights for selected language!`);
     }
   }
 
   /**
    * Function to check that there are no translators for the selected language.
-   * @param {string} usename - the user expected to not be displayed.
    */
-  async expectUserToNotBeDisplayed(username) {
+  async expectUserToNotBeDisplayed(username: string): Promise<void> {
     await this.page.waitForSelector(viewLanguageRoleUserResult);
     const displayedUsers = await this.page.$eval(
       viewLanguageRoleUserResult,
-      element => element.innerText
+      element => (element as HTMLElement).innerText
     );
     if (displayedUsers.includes(username)) {
       throw new Error(
-        `${user} has translation rights for selected language!`);
+        `${username} has translation rights for selected language!`);
     }
   }
-};
+}
+
+export let TranslationAdminFactory =
+  (): ITranslationAdmin => new TranslationAdmin();
diff --git a/scripts/run_acceptance_tests.py b/scripts/run_acceptance_tests.py
--- a/scripts/run_acceptance_tests.py
+++ b/scripts/run_acceptance_tests.py
@@ -19,6 +19,7 @@
 import argparse
 import contextlib
 import os
+import shutil
 import subprocess
 import sys
 
@@ -61,6 +62,37 @@
     help='Build webpack with source maps.',
     action='store_true')
 
+_PARSER.add_argument(
+    '--headless',
+    help='Run the tests in headless mode.',
+    action='store_true')
+
+
+def compile_test_ts_files() -> None:
+    """Compiles the test typescript files into a build directory."""
+    puppeteer_acceptance_tests_dir_path = os.path.join(
+        common.CURR_DIR, 'core', 'tests', 'puppeteer-acceptance-tests')
+    build_dir_path = os.path.join(puppeteer_acceptance_tests_dir_path, 'build')
+
+    if os.path.exists(build_dir_path):
+        shutil.rmtree(build_dir_path)
+
+    cmd = (
+        './node_modules/typescript/bin/tsc -p %s' %
+        './tsconfig.puppeteer-acceptance-tests.json')
+    proc = subprocess.Popen(
+        cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
+
+    _, encoded_stderr = proc.communicate()
+    stderr = encoded_stderr.decode('utf-8')
+
+    if stderr:
+        raise Exception(stderr)
+
+    shutil.copytree(
+        os.path.join(puppeteer_acceptance_tests_dir_path, 'images'),
+        os.path.join(build_dir_path, 'images'))
+
 
 def run_tests(args: argparse.Namespace) -> Tuple[List[bytes], int]:
     """Run the scripts to start acceptance tests."""
@@ -74,6 +106,7 @@ def run_tests(args: argparse.Namespace) -> Tuple[List[bytes], int]:
     with contextlib.ExitStack() as stack:
         dev_mode = not args.prod_env
 
+        compile_test_ts_files()
         if args.skip_build:
             common.modify_constants(prod_env=args.prod_env)
         else:
@@ -104,6 +137,7 @@ def run_tests(args: argparse.Namespace) -> Tuple[List[bytes], int]:
 
         proc = stack.enter_context(servers.managed_acceptance_tests_server(
             suite_name=args.suite,
+            headless=args.headless,
             stdout=subprocess.PIPE))
 
         print('Servers have come up.\n')
diff --git a/scripts/run_acceptance_tests_test.py b/scripts/run_acceptance_tests_test.py
--- a/scripts/run_acceptance_tests_test.py
+++ b/scripts/run_acceptance_tests_test.py
@@ -28,7 +28,18 @@
 from scripts import scripts_test_utils
 from scripts import servers
 
-from typing import ContextManager, Optional
+from typing import ContextManager, List, Optional, Tuple
+
+
+class PopenErrorReturn:
+    """Popen return object."""
+
+    def __init__(self) -> None:
+        self.returncode = 1
+
+    def communicate(self) -> Tuple[str, bytes]:
+        """Returns some error."""
+        return '', 'Some error'.encode('utf-8')
 
 
 def mock_managed_long_lived_process(
@@ -90,6 +101,18 @@ def tearDown(self) -> None:
         finally:
             super().tearDown()
 
+    def test_compile_test_ts_files_with_error(self) -> None:
+        def mock_popen_error_call(
+            unused_cmd_tokens: List[str], *args: str, **kwargs: str # pylint: disable=unused-argument
+        ) -> PopenErrorReturn:
+            return PopenErrorReturn()
+
+        popen_error_swap = self.swap(
+            subprocess, 'Popen', mock_popen_error_call)
+        with popen_error_swap:
+            with self.assertRaisesRegex(Exception, 'Some error'):
+                run_acceptance_tests.compile_test_ts_files()
+
     def test_start_tests_when_other_instances_not_stopped(self) -> None:
         self.exit_stack.enter_context(self.swap_with_checks(
             common, 'is_oppia_server_already_running', lambda *_: True))
@@ -126,6 +149,7 @@ def test_start_tests_when_no_other_instance_running(self) -> None:
             expected_kwargs=[
                 {
                     'suite_name': 'testSuite',
+                    'headless': False,
                     'stdout': subprocess.PIPE,
                 },
             ]))
@@ -165,6 +189,7 @@ def mock_managed_acceptance_tests_server(
             expected_kwargs=[
                 {
                     'suite_name': 'testSuite',
+                    'headless': False,
                     'stdout': subprocess.PIPE,
                 },
             ]))
@@ -206,6 +231,7 @@ def test_start_tests_skip_build(self) -> None:
             expected_kwargs=[
                 {
                     'suite_name': 'testSuite',
+                    'headless': False,
                     'stdout': subprocess.PIPE,
                 },
             ]))
@@ -237,6 +263,7 @@ def test_start_tests_in_jasmine(self) -> None:
             expected_kwargs=[
                 {
                     'suite_name': 'testSuite',
+                    'headless': False,
                     'stdout': subprocess.PIPE,
                 },
             ]))
@@ -271,6 +298,7 @@ def test_start_tests_with_emulator_mode_false(self) -> None:
             expected_kwargs=[
                 {
                     'suite_name': 'testSuite',
+                    'headless': False,
                     'stdout': subprocess.PIPE,
                 },
             ]))
@@ -304,6 +332,7 @@ def test_start_tests_for_long_lived_process(self) -> None:
             expected_kwargs=[
                 {
                     'suite_name': 'testSuite',
+                    'headless': False,
                     'stdout': subprocess.PIPE,
                 },
             ]))
diff --git a/scripts/servers_test.py b/scripts/servers_test.py
--- a/scripts/servers_test.py
+++ b/scripts/servers_test.py
@@ -1077,7 +1077,8 @@ def mock_urlopen(
 
     def test_managed_acceptance_test_server_with_explicit_args(self) -> None:
         popen_calls = self.exit_stack.enter_context(self.swap_popen())
-        test_file_path = 'blog-admin-tests/assign-roles-to-users-and-change-tag-properties.spec.js' # pylint: disable=line-too-long
+        test_file_path = (
+            'blog-admin-tests/assign-roles-to-users-and-change-tag-properties')
 
         self.exit_stack.enter_context(servers.managed_acceptance_tests_server(
             suite_name=test_file_path,
@@ -1088,7 +1089,7 @@ def test_managed_acceptance_test_server_with_explicit_args(self) -> None:
         self.assertEqual(
             popen_calls[0].kwargs, {'shell': True, 'stdout': subprocess.PIPE})
         program_args = popen_calls[0].program_args
-        self.assertIn(test_file_path, program_args)
+        self.assertIn('%s.spec.js' % test_file_path, program_args)
 
     def test_managed_acceptance_test_server_with_invalid_suite(self) -> None:
         suite_name = 'invalid_suite'
@@ -1100,6 +1101,19 @@ def test_managed_acceptance_test_server_with_invalid_suite(self) -> None:
                     suite_name=suite_name,
                     stdout=subprocess.PIPE))
 
+    def test_managed_acceptance_test_server_mobile(
+        self
+    ) -> None:
+        suite_name = (
+            'blog-admin-tests/assign-roles-to-users-and-change-tag-properties')
+
+        with self.exit_stack.enter_context(
+            servers.managed_acceptance_tests_server(
+                suite_name=suite_name,
+                headless=True,
+                stdout=subprocess.PIPE)):
+            self.assertEqual(os.getenv('HEADLESS'), 'true')
+
 
 class GetChromedriverVersionTests(test_utils.TestBase):