Repository: oppia/oppia

Issue:
[FE flake] Context service behavior
## Logs

```
Chrome Headless 111.0.5563.110 (Linux x86_64) Context service behavior in the blog home pages should correctly retrieve the blog post id FAILED
	Error: Expected 'undefined' to be 'sample123456'.
	    at <Jasmine>
	    at UserContext.<anonymous> (core/templates/combined-tests.spec.js:213059:39)
	    at ./node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke (core/templates/combined-tests.spec.js:770112:30)
	    at ./node_modules/zone.js/dist/proxy.js.ProxyZoneSpec.onInvoke (core/templates/combined-tests.spec.js:769597:43)
Chrome Headless 111.0.5563.110 (Linux x86_64): Executed 628 of 8511 (3 FAILED) (0 secs / 15.647 secs)
Chrome Headless 111.0.5563.110 (Linux x86_64) Context service behavior in the blog home pages should correctly retrieve the blog post id FAILED
	Error: Expected 'undefined' to be 'sample123456'.
	    at <Jasmine>
	    at UserContext.<anonymous> (core/templates/combined-tests.spec.js:213059:39)
	    at ./node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke (core/templates/combined-tests.spec.js:770112:30)
	    at ./node_modules/zone.js/dist/proxy.js.ProxyZoneSpec.onInvoke (core/templates/combined-tests.spec.js:769597:43)
Chrome Headless 111.0.5563.110 (Linux x86_64): Executed 629 of 8511 (3 FAILED) (0 secs / 15.651 secs)
Chrome Headless 111.0.5563.110 (Linux x86_64): Executed 630 of 8511 (3 FAILED) (0 secs / 15.654 secs)
Chrome Headless 111.0.5563.110 (Linux x86_64): Executed 631 of 8511 (3 FAILED) (0 secs / 15.656 secs)
Chrome Headless 111.0.5563.110 (Linux x86_64): Executed 632 of 8511 (3 FAILED) (0 secs / 15.659 secs)
Chrome Headless 111.0.5563.110 (Linux x86_64) Context service behavior in the skill editor view should correctly retrieve the skill id FAILED
	Error: Expected 'undefined' to be '123'.
	    at <Jasmine>
	    at UserContext.<anonymous> (core/templates/combined-tests.spec.js:212991:39)
	    at ./node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke (core/templates/combined-tests.spec.js:770112:30)
	    at ./node_modules/zone.js/dist/proxy.js.ProxyZoneSpec.onInvoke (core/templates/combined-tests.spec.js:769597:43)
Chrome Headless 111.0.5563.110 (Linux x86_64): Executed 633 of 8511 (4 FAILED) (0 secs / 15.672 secs)
Chrome Headless 111.0.5563.110 (Linux x86_64) Context service behavior in the skill editor view should correctly retrieve the skill id FAILED
	Error: Expected 'undefined' to be '123'.
	    at <Jasmine>
	    at UserContext.<anonymous> (core/templates/combined-tests.spec.js:212991:39)
	    at ./node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke (core/templates/combined-tests.spec.js:770112:30)
	    at ./node_modules/zone.js/dist/proxy.js.ProxyZoneSpec.onInvoke (core/templates/combined-tests.spec.js:769597:43)
Chrome Headless 111.0.5563.110 (Linux x86_64): Executed 634 of 8511 (4 FAILED) (0 secs / 15.675 secs)
Chrome Headless 111.0.5563.110 (Linux x86_64): Executed 635 of 8511 (4 FAILED) (0 secs / 15.679 secs)
Chrome Headless 111.0.5563.110 (Linux x86_64): Executed 636 of 8511 (4 FAILED) (0 secs / 15.682 secs)
Chrome Headless 111.0.5563.110 (Linux x86_64) Context service behavior in the story editor view should correctly retrieve the story id FAILED
	Error: Expected 'undefined' to be '123'.
	    at <Jasmine>
	    at UserContext.<anonymous> (core/templates/combined-tests.spec.js:212960:39)
	    at ./node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke (core/templates/combined-tests.spec.js:770112:30)
	    at ./node_modules/zone.js/dist/proxy.js.ProxyZoneSpec.onInvoke (core/templates/combined-tests.spec.js:769597:43)
Chrome Headless 111.0.5563.110 (Linux x86_64): Executed 637 of 8511 (5 FAILED) (0 secs / 15.685 secs)
Chrome Headless 111.0.5563.110 (Linux x86_64) Context service behavior in the story editor view should correctly retrieve the story id FAILED
	Error: Expected 'undefined' to be '123'.
	    at <Jasmine>
	    at UserContext.<anonymous> (core/templates/combined-tests.spec.js:212960:39)
	    at ./node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke (core/templates/combined-tests.spec.js:770112:30)
	    at ./node_modules/zone.js/dist/proxy.js.ProxyZoneSpec.onInvoke (core/templates/combined-tests.spec.js:769597:43)
```

The spec file test:
https://github.com/oppia/oppia/blob/30b992dc318fd3d29f9a04aca130c7dda3858e07/core/templates/services/context.service.spec.ts#L193-L197

## Occurences

- https://github.com/oppia/oppia/actions/runs/4601108033
- https://github.com/oppia/oppia/actions/runs/4643258041/jobs/8217776341
- https://github.com/oppia/oppia/actions/runs/4691366727/jobs/8315750486
- https://github.com/oppia/oppia/actions/runs/4658275541/jobs/8243763868

Incorrect Description of the Hausa Language Title on language dropdown
Please edit 'Halshen Hausa' to 'Harshen Hausa' as that is the correct way to say 'Hausa Language/Tongue' in Hausa.

![Hausa language](https://github.com/oppia/oppia/assets/115101990/2fa08e61-0694-4bd5-aae0-0952442dd551)



Discussion:

Resolving patch:
diff --git a/scripts/common.py b/scripts/common.py
--- a/scripts/common.py
+++ b/scripts/common.py
@@ -215,7 +215,10 @@
 ACCEPTANCE_TESTS_SUITE_NAMES = [
     'blog-admin-tests/assign-roles-to-users-and-change-tag-properties.spec.js',
     'blog-editor-tests/check-blog-editor-unable-to-publish-' +
-    'duplicate-blog-post.spec.js'
+    'duplicate-blog-post.spec.js',
+    'translation-admin-tests/add-translation-rights.spec.js',
+    'translation-admin-tests/remove-translation-rights.spec.js'
+
 ]
 
 GAE_PORT_FOR_E2E_TESTING: Final = 9001


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
@@ -302,6 +302,8 @@ jobs:
           suite:
             - blog-admin-tests/assign-roles-to-users-and-change-tag-properties.spec.js
             - blog-editor-tests/check-blog-editor-unable-to-publish-duplicate-blog-post.spec.js
+            - translation-admin-tests/add-translation-rights.spec.js
+            - translation-admin-tests/remove-translation-rights.spec.js
       steps:
         - uses: actions/checkout@v3
         - uses: actions/setup-python@v3
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.js
--- a/core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/puppeteer-utils.js
+++ b/core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/puppeteer-utils.js
@@ -134,6 +134,16 @@ module.exports = class baseUser {
     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) {
+    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.
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.js
--- a/core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/test-constants.js
+++ b/core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/test-constants.js
@@ -24,6 +24,7 @@ let testConstants = {
     BlogDashboard: 'http://localhost:9001/blog-dashboard',
     BlogAdmin: 'http://localhost:9001/blog-admin',
     CreatorDashboard: 'http://localhost:9001/creator-dashboard',
+    ContributorDashboardAdmin: 'http://localhost:9001/contributor-dashboard-admin/',
     AdminPage: 'http://localhost:9001/admin',
     RolesEditorTab: 'http://localhost:9001/admin#/roles',
     logout: 'http://localhost:9001/logout'
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
--- a/core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/user-factory.js
+++ b/core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/user-factory.js
@@ -19,6 +19,8 @@
 let e2eSuperAdmin = require('../user-utilities/super-admin-utils.js');
 let e2eBlogAdmin = e2eBlogPostEditor = e2eGuestUser = require(
   '../user-utilities/blog-post-admin-utils.js');
+let e2eTranslationAdmin = require(
+  '../user-utilities/translation-admin-utils.js');
 
 /**
  * Global user instances that are created and can be reused again.
@@ -27,6 +29,7 @@ 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';
 
 /**
  * The function creates a new super admin user and returns the instance
@@ -108,6 +111,32 @@ let createNewGuestUser = async function(username, email) {
   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;
+};
+
 /**
  * The function closes all the browsers opened by different users.
  */
@@ -117,10 +146,23 @@ let closeAllBrowsers = async function() {
   }
 };
 
+/**
+ * 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,
-  closeAllBrowsers
+  createNewTranslationAdmin,
+  closeAllBrowsers,
+  closeBrowserForUser
 };
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.js
new file mode 100644
--- /dev/null
+++ b/core/tests/puppeteer-acceptance-tests/spec/translation-admin-tests/add-translation-rights.spec.js
@@ -0,0 +1,55 @@
+// 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 Acceptance Test for translation admins to add translation
+ * rights to users.
+ */
+
+const userFactory = require(
+  '../../puppeteer-testing-utilities/user-factory.js');
+const testConstants = require(
+  '../../puppeteer-testing-utilities/test-constants.js');
+
+const DEFAULT_SPEC_TIMEOUT = testConstants.DEFAULT_SPEC_TIMEOUT;
+
+describe('Translation Admin', function() {
+  let translationAdmin = null;
+
+  beforeAll(async function() {
+    translationAdmin =
+      await userFactory.createNewTranslationAdmin('translationAdm');
+  }, DEFAULT_SPEC_TIMEOUT);
+
+  it('should be able to provide translation rights to user.',
+    async function() {
+      let translatorSpanish = await userFactory.createNewGuestUser(
+        'translatorSpanish', 'translatorSpanish@example.com');
+      await userFactory.closeBrowserForUser(translatorSpanish);
+      await translationAdmin.navigateToContributorDashboardAdminPage();
+
+      await translationAdmin.addTranslationLanguageReviewRights(
+        'translatorSpanish', 'es');
+
+      await translationAdmin.viewContributionRightsForUser('translatorSpanish');
+      await translationAdmin.expectDisplayedLanguagesToContain('Spanish');
+      await translationAdmin.viewContributorTranslationRightsByLanguageCode(
+        'es');
+      await translationAdmin.expectUserToBeDisplayed('translatorSpanish');
+    }, DEFAULT_SPEC_TIMEOUT);
+
+  afterAll(async function() {
+    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.js
new file mode 100644
--- /dev/null
+++ b/core/tests/puppeteer-acceptance-tests/spec/translation-admin-tests/remove-translation-rights.spec.js
@@ -0,0 +1,58 @@
+// 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 Acceptance Test for translation admins to remove translation
+ * rights from users.
+ */
+
+const userFactory = require(
+  '../../puppeteer-testing-utilities/user-factory.js');
+const testConstants = require(
+  '../../puppeteer-testing-utilities/test-constants.js');
+
+const DEFAULT_SPEC_TIMEOUT = testConstants.DEFAULT_SPEC_TIMEOUT;
+
+describe('Translation Admin', function() {
+  let translationAdmin = null;
+
+  beforeAll(async function() {
+    translationAdmin =
+      await userFactory.createNewTranslationAdmin('translationAdm');
+  }, DEFAULT_SPEC_TIMEOUT);
+
+  it('should be able to remove translation rights from user.',
+    async function() {
+      let translatorSpanish = await userFactory.createNewGuestUser(
+        'translatorSpanish', 'translatorSpanish@example.com');
+      await userFactory.closeBrowserForUser(translatorSpanish);
+      await translationAdmin.navigateToContributorDashboardAdminPage();
+      await translationAdmin.addTranslationLanguageReviewRights(
+        'translatorSpanish', 'es');
+
+      await translationAdmin.viewContributionRightsForUser('translatorSpanish');
+      await translationAdmin.expectDisplayedLanguagesToContain('Spanish');
+
+      await translationAdmin.removeTranslationLanguageReviewRights(
+        'translatorSpanish', 'es');
+
+      await translationAdmin.viewContributorTranslationRightsByLanguageCode(
+        'es');
+      await translationAdmin.expectUserToNotBeDisplayed('translatorSpanish');
+    }, DEFAULT_SPEC_TIMEOUT);
+
+  afterAll(async function() {
+    await userFactory.closeAllBrowsers();
+  });
+});
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.js
new file mode 100644
--- /dev/null
+++ b/core/tests/puppeteer-acceptance-tests/user-utilities/translation-admin-utils.js
@@ -0,0 +1,182 @@
+// 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 Translation admin users utility file.
+ */
+
+const baseUser = require(
+  '../puppeteer-testing-utilities/puppeteer-utils.js');
+const testConstants = require(
+  '../puppeteer-testing-utilities/test-constants.js');
+const ContributorDashboardAdminUrl =
+  testConstants.URLs.ContributorDashboardAdmin;
+const { showMessage } = require(
+  '../puppeteer-testing-utilities/show-message-utils.js');
+
+const translationRightValue = 'string:translation';
+const usernameMethodValue = 'username';
+const roleMethodValue = 'role';
+
+// "View Contributor Dashboard Users" form elements.
+const viewContributorFilterMethodSelect =
+  'select#view-contributor-filter-method-select';
+const viewContributerUsernameInput =
+  'input#view-contributor-username-input';
+const viewContributorCategorySelect =
+  'select#view-contributor-category-select';
+const viewContributorLanguageSelect =
+  'select#view-contributor-language-select';
+const viewContributorSubmitButton =
+  'button#view-contributor-submit-button';
+const viewContributorLanguageResult =
+  '.e2e-test-translation-reviewer-language';
+const viewLanguageRoleUserResult =
+  '.e2e-test-reviewer-roles-result';
+
+// "Add Contribution Rights" form elements.
+const addContributorUsernameInput =
+  'input#add-contribution-rights-user-input';
+const addContributonRightsCategorySelect =
+  'select#add-contribution-rights-category-select';
+const addContributonRightsLanguageDropdown =
+  'select#add-contribution-rights-language-select';
+const addContributionRightsSubmitButton =
+   'button#add-contribution-rights-submit-button';
+
+// "Remove Contribution Rights" form elements.
+const removeContributorUsernameInput =
+  'input#remove-contribution-rights-user-input';
+const removeContributonRightsCategorySelect =
+  'select#remove-contribution-rights-category-select';
+const removeContributonRightsLanguageSelect =
+  'select#remove-contribution-rights-language-select';
+const removeContributionRightsSubmitButton =
+  'button#remove-contribution-rights-submit-button';
+
+module.exports = class TranslationAdmin extends baseUser {
+  /**
+   * Function for navigating to the contributor dashboard admin page.
+   */
+  async navigateToContributorDashboardAdminPage() {
+    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) {
+    await this.type(addContributorUsernameInput, username);
+    await this.select(
+      addContributonRightsCategorySelect, translationRightValue);
+    await this.select(
+      addContributonRightsLanguageDropdown, 'string:' + languageCode);
+    await this.clickOn(addContributionRightsSubmitButton);
+
+    await this.page.waitForNetworkIdle();
+  }
+
+  /**
+   * 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) {
+    await this.type(removeContributorUsernameInput, username);
+    await this.select(
+      removeContributonRightsCategorySelect, translationRightValue);
+    await this.select(
+      removeContributonRightsLanguageSelect, 'string:' + languageCode);
+    await this.clickOn(removeContributionRightsSubmitButton);
+
+    await this.page.waitForNetworkIdle();
+  }
+
+  /**
+   * Function to display contribution rights by user.
+   * @param {string} username - the username of the user to view.
+   */
+  async viewContributionRightsForUser(username) {
+    await this.select(viewContributorFilterMethodSelect, usernameMethodValue);
+    await this.type(viewContributerUsernameInput, username);
+    await this.clickOn(viewContributorSubmitButton);
+
+    await this.page.waitForNetworkIdle();
+  }
+
+  /**
+   * Function to display translation rights by language.
+   * @param {string} languageCode - the language option as an option value.
+   */
+  async viewContributorTranslationRightsByLanguageCode(languageCode) {
+    await this.select(viewContributorFilterMethodSelect, roleMethodValue);
+    await this.select(viewContributorCategorySelect, translationRightValue);
+    await this.select(viewContributorLanguageSelect, 'string:' + languageCode);
+    await this.clickOn(viewContributorSubmitButton);
+
+    await this.page.waitForNetworkIdle();
+  }
+
+  /**
+   * Function to check if the language is displayed as a translation right.
+   * @param {string} language - the language to match.
+   */
+  async expectDisplayedLanguagesToContain(language) {
+    await this.page.waitForSelector(viewContributorLanguageResult);
+    const displayedLanguage = await this.page.$eval(
+      viewContributorLanguageResult,
+      element => element.innerText);
+    if (!displayedLanguage.includes(language)) {
+      throw new Error(
+        `Selected user does not have translation rights for ${language}!`);
+    } else {
+      showMessage(
+        `Selected user has translation rights for ${displayedLanguage}`);
+    }
+  }
+
+  /**
+   * Function to check if the user is displayed as a translator.
+   * @param {string} usenamer - the user expected to be displayed.
+   */
+  async expectUserToBeDisplayed(username) {
+    await this.page.waitForSelector(viewLanguageRoleUserResult);
+    const displayedUsers = await this.page.$eval(
+      viewLanguageRoleUserResult,
+      element => element.innerText
+    );
+    if (!displayedUsers.includes(username)) {
+      throw new Error(
+        `${user} 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) {
+    await this.page.waitForSelector(viewLanguageRoleUserResult);
+    const displayedUsers = await this.page.$eval(
+      viewLanguageRoleUserResult,
+      element => element.innerText
+    );
+    if (displayedUsers.includes(username)) {
+      throw new Error(
+        `${user} has translation rights for selected language!`);
+    }
+  }
+};