Repository: oppia/oppia

Issue:
Backend test flake: mis-ordering of exploration states
Example: https://github.com/oppia/oppia/actions/runs/3476372502/jobs/5811542387#step:8:1949

Log:

```
======================================================================
FAIL: test_verify_all_states_reachable (core.domain.exp_domain_test.ExplorationDomainUnitTests)
----------------------------------------------------------------------
core.utils.ValidationError: Please fix the following issues before saving this exploration: 1. The following states are not reachable from the initial state: End 2. It is impossible to complete the exploration from the following states: Introduction, Stuck State 

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/runner/work/oppia/oppia/core/domain/exp_domain_test.py", line 4206, in test_verify_all_states_reachable
    exploration.validate(strict=True)
AssertionError: "Please fix the following issues before saving this exploration: 1. The following states are not reachable from the initial state: End 2. It is impossible to complete the exploration from the following states: Stuck State, Introduction" does not match "Please fix the following issues before saving this exploration: 1. The following states are not reachable from the initial state: End 2. It is impossible to complete the exploration from the following states: Introduction, Stuck State "
```

We likely need to change our assertion so that the order of states does not matter.
[Feature Request]: Changing the Twitter in footer
### Is your feature request related to a problem? Please describe.

Since Twitter has replaced its old Bird logo with its new X logo it shall also be changed in the footer section of the oppia website 

### Describe the solution (or solutions) you'd like

Changing the old Twitter Bird logo to its new X logo 


### Describe alternatives you've considered and rejected

_No response_

### Additional context



![image](https://github.com/oppia/oppia/assets/124715224/b7d0b3f8-eb89-4fab-8eeb-e5e1fa6136ba)

[BUG]: Concept card button not functioning
### Describe the bug

When the concept card icon is clicked, it is unresponsive, and no content is displayed.

### Steps To Reproduce

Play a lesson, e.g. [Addition and subtraction lesson 1](https://www.oppia.org/explore/OKxYhsWONHZV?story_url_fragment=maya-omar-malik-make-pizza&topic_url_fragment=addition-subtraction&classroom_url_fragment=math&node_id=node_1#)

When you reach the card with the state name: Question 2, once the purple concept card icon pops up, click on it.

### Expected Behavior

This is a bug, because concept cards are not usually linked this way, only hints are.

### Screenshots/Videos

https://www.loom.com/share/d0e3c6112c7341c980c353af41e9a880

### What device are you using?

Desktop

### Operating System

Windows

### What browsers are you seeing the problem on?

Chrome

### Browser version

110.0.5481.178

### Additional context

_No response_


Discussion:


@Priyansh61 @kshitij01042002 Flagging this since it looks like a serious regression (a learner can get stuck).
Want to work on this
Hi @A-R-M-028 thanks for your consideration to solve this issue.
Per the guidance at https://github.com/oppia/oppia/wiki/Contributing-code-to-Oppia#choosing-a-good-first-issue, please provide an explanation of what your PR will do (with names of files you're changing, what you plan to change in each file, etc.). If it looks good, we can assign you to this issue.

Please also follow the other instructions on that wiki page if you have not yet done so. Thanks!
Hi, @seanlip, @Priyansh61, @Shivkant-Chauhan I have tried to fix this issue by adding the hint box inside the concept-card button because when I saw the code the button was displayed first and then only the hint box was there so due to this reason the button was not clicking.
The file I have changed is `/oppia/core/templates/pages/exploration-player-page/layout-directives/exploration-footer.component.html`

video demonstrating :

https://user-images.githubusercontent.com/92446645/228248075-3bf7c3b5-1624-4495-85c7-48974d488a31.mp4

>
Before the changes :
<img width="953" alt="Screenshot 2023-03-28 at 6 17 17 PM" src="https://user-images.githubusercontent.com/92446645/228248202-aabce878-c01a-4bdd-80bf-8d12f9b95eab.png">

After the changes : 

<img width="956" alt="Screenshot 2023-03-28 at 6 16 46 PM" src="https://user-images.githubusercontent.com/92446645/228248240-a3dfd33a-1816-4e2e-ae78-7bbe276c407f.png">

<img width="960" alt="Screenshot 2023-03-28 at 6 18 30 PM" src="https://user-images.githubusercontent.com/92446645/228248157-8a89b22b-5d8d-43bc-89f0-33a4f6ceb3e7.png">



@hemantmm Great Work! Assigning you the issue. Thanks
Closing this as fixed by PR  https://github.com/oppia/oppia/pull/17866
Re-opening this. This issue still persists.
Hi @Kebith, Can you please try this and let me know if this is working on your side.
https://github.com/oppia/oppia/assets/109658545/a4ea3b69-5814-493e-85b3-3c3e25acf98d


Hi @prafulbbandre This issue still persists.
![Screenshot 2023-09-29 at 18-24-30 Introduction to Addition - Part 1](https://github.com/oppia/oppia/assets/111491620/b34d9940-d862-41c1-955c-3746fa6ce1d8)


Resolving patch:
diff --git a/scripts/common.py b/scripts/common.py
--- a/scripts/common.py
+++ b/scripts/common.py
@@ -216,6 +216,7 @@
     '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',
+    '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'
 


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,7 @@ 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
+            - 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
       steps:
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
@@ -21,6 +21,8 @@ let e2eBlogAdmin = e2eBlogPostEditor = e2eGuestUser = 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.
@@ -30,6 +32,7 @@ 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
@@ -137,6 +140,32 @@ let createNewTranslationAdmin = async function(username, email) {
   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.
  */
@@ -163,6 +192,7 @@ module.exports = {
   createNewBlogPostEditor,
   createNewGuestUser,
   createNewTranslationAdmin,
+  createNewQuestionAdmin,
   closeAllBrowsers,
   closeBrowserForUser
 };
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.js
new file mode 100644
--- /dev/null
+++ b/core/tests/puppeteer-acceptance-tests/spec/practice-question-admin-tests/add-and-remove-contribution-rights.spec.js
@@ -0,0 +1,67 @@
+// 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 question admins to add and remove
+ * reviewing question 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('Question Admin', function() {
+  let questionAdmin = null;
+
+  beforeAll(async function() {
+    questionAdmin =
+      await userFactory.createNewQuestionAdmin('questionAdm');
+  }, DEFAULT_SPEC_TIMEOUT);
+
+  it('should be able to provide rights to review and submit questions to user.',
+    async function() {
+      let Tester = await userFactory.createNewGuestUser(
+        'Tester', 'admin.tester@example.com');
+      await userFactory.closeBrowserForUser(Tester);
+
+      await questionAdmin.navigateToContributorDashboardAdminPage();
+      await questionAdmin.verifyUserCannotReviewQuestions('Tester');
+      await questionAdmin.verifyQuestionReviewersExcludeUser('Tester');
+      await questionAdmin.verifyUserCannotSubmitQuestions('Tester');
+      await questionAdmin.verifyQuestionSubmittersExcludeUser('Tester');
+
+      await questionAdmin.addSubmitQuestionRights('Tester');
+      await questionAdmin.addReviewQuestionRights('Tester');
+
+      await questionAdmin.verifyUserCanReviewQuestions('Tester');
+      await questionAdmin.verifyQuestionReviewersIncludeUser('Tester');
+      await questionAdmin.verifyUserCanSubmitQuestions('Tester');
+      await questionAdmin.verifyQuestionSubmittersIncludeUser('Tester');
+
+      await questionAdmin.removeSubmitQuestionRights('Tester');
+      await questionAdmin.removeReviewQuestionRights('Tester');
+
+      await questionAdmin.verifyUserCannotReviewQuestions('Tester');
+      await questionAdmin.verifyQuestionReviewersExcludeUser('Tester');
+      await questionAdmin.verifyUserCannotSubmitQuestions('Tester');
+      await questionAdmin.verifyQuestionSubmittersExcludeUser('Tester');
+    }, DEFAULT_SPEC_TIMEOUT);
+
+  afterAll(async function() {
+    await userFactory.closeAllBrowsers();
+  });
+});
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.js
new file mode 100644
--- /dev/null
+++ b/core/tests/puppeteer-acceptance-tests/user-utilities/question-admin-utils.js
@@ -0,0 +1,310 @@
+// 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 Question 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 reviewQuestionRightValue = 'string:question';
+const submitQuestionRightValue = 'string:submit_question';
+const usernameMethodValue = 'username';
+const roleMethodValue = 'role';
+
+// "View Contributor Dashboard Users" form elements.
+const viewContributorFilterMethodSelector =
+  'select#view-contributor-filter-method-select';
+const viewContributerUsernameInput =
+  'input#view-contributor-username-input';
+const viewContributorCategorySelector =
+  'select#view-contributor-category-select';
+const viewContributorSubmitButton =
+  'button#view-contributor-submit-button';
+
+const viewContributorReviewQuestionsResult =
+  '.e2e-test-question-reviewer';
+const viewContributorSubmitQuestionResult =
+  '.e2e-test-question-contributor';
+const viewRoleUserResult =
+  '.e2e-test-reviewer-roles-result';
+
+// "Add Contribution Rights" form elements.
+const addContributorUsernameInput =
+  'input#add-contribution-rights-user-input';
+const addContributonRightsCategorySelector =
+  'select#add-contribution-rights-category-select';
+const addContributionRightsSubmitButton =
+   'button#add-contribution-rights-submit-button';
+
+// "Remove Contribution Rights" form elements.
+const removeContributorUsernameInput =
+  'input#remove-contribution-rights-user-input';
+const removeContributonRightsCategorySelector =
+  'select#remove-contribution-rights-category-select';
+const removeContributionRightsSubmitButton =
+  'button#remove-contribution-rights-submit-button';
+
+module.exports = class QuestionAdmin extends baseUser {
+  /**
+   * Function for navigating to the contributor dashboard admin page.
+   */
+  async navigateToContributorDashboardAdminPage() {
+    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) {
+    await this.type(addContributorUsernameInput, username);
+    await this.select(
+      addContributonRightsCategorySelector, reviewQuestionRightValue);
+    await this.clickOn(addContributionRightsSubmitButton);
+
+    await this.page.waitForNetworkIdle();
+  }
+
+  /**
+   * Function for adding a right of submitting questions to a user.
+   * @param {string} username - the username of the user.
+   */
+  async addSubmitQuestionRights(username) {
+    await this.type(addContributorUsernameInput, username);
+    await this.select(
+      addContributonRightsCategorySelector, submitQuestionRightValue);
+    await this.clickOn(addContributionRightsSubmitButton);
+
+    await this.page.waitForNetworkIdle();
+  }
+
+  /**
+   * Function for removng a right of reviewing questions to a user.
+   * @param {string} username - the username of the user.
+   */
+  async removeReviewQuestionRights(username) {
+    await this.type(removeContributorUsernameInput, username);
+    await this.select(
+      removeContributonRightsCategorySelector, reviewQuestionRightValue);
+    await this.clickOn(removeContributionRightsSubmitButton);
+
+    await this.page.waitForNetworkIdle();
+  }
+
+  /**
+   * Function for removing a right of reviewing questions to a user.
+   * @param {string} username - the username of the user.
+   */
+  async removeSubmitQuestionRights(username) {
+    await this.type(removeContributorUsernameInput, username);
+    await this.select(
+      removeContributonRightsCategorySelector, submitQuestionRightValue);
+    await this.clickOn(removeContributionRightsSubmitButton);
+
+    await this.page.waitForNetworkIdle();
+  }
+
+  /**
+   * Function to return the list of question reviewers
+   * @returns {string[]} displayedUsers - list of strings of all username
+   */
+  async getDisplayedListOfQuestionReviewers() {
+    await this.select(viewContributorFilterMethodSelector, roleMethodValue);
+    await this.select(
+      viewContributorCategorySelector, reviewQuestionRightValue);
+    await this.clickOn(viewContributorSubmitButton);
+
+    await this.page.waitForNetworkIdle();
+
+    await this.page.waitForSelector(viewRoleUserResult);
+    const displayedUsers = await this.page.$eval(
+      viewRoleUserResult,
+      element => element.innerText
+    );
+    return displayedUsers;
+  }
+
+  /**
+   * Function to return the list of question reviewers
+   * @returns {string[]} displayedUsers - list of strings of all username
+   */
+  async getDisplayedListOfQuestionSubmitters() {
+    await this.select(viewContributorFilterMethodSelector, roleMethodValue);
+    await this.select(
+      viewContributorCategorySelector, submitQuestionRightValue);
+    await this.clickOn(viewContributorSubmitButton);
+
+    await this.page.waitForNetworkIdle();
+
+    await this.page.waitForSelector(viewRoleUserResult);
+    const displayedUsers = await this.page.$eval(
+      viewRoleUserResult,
+      element => element.innerText
+    );
+    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) {
+    await this.select(viewContributorFilterMethodSelector, usernameMethodValue);
+    await this.type(viewContributerUsernameInput, username);
+    await this.clickOn(viewContributorSubmitButton);
+
+    await this.page.waitForNetworkIdle();
+
+    await this.page.waitForSelector(contribution);
+    const contributionStatusForUser = await this.page.$eval(
+      contribution,
+      element => element.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(
+      username, viewContributorReviewQuestionsResult);
+
+    if (questionReviewStatusForUser === 'Not-allowed') {
+      throw new Error(
+        `${username} does not have rights for reviewing questions!`);
+    } else {
+      showMessage(
+        `${username} has rights for reviewing questions.`);
+    }
+  }
+
+  /**
+   * 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(
+      username, viewContributorSubmitQuestionResult);
+
+    if (questionSubmitStatusForUser === 'Not-allowed') {
+      throw new Error(
+        `${username} does not have rights for submitting questions!`);
+    } else {
+      showMessage(
+        `${username} has rights for submitting questions.`);
+    }
+  }
+
+  /**
+   * 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(
+      username, viewContributorReviewQuestionsResult);
+
+    if (questionReviewStatusForUser === 'Allowed') {
+      throw new Error(
+        `${username} has rights for reviewing questions!`);
+    } else {
+      showMessage(
+        `${username} doesn't have rights for reviewing questions.`);
+    }
+  }
+
+  /**
+   * 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(
+      username, viewContributorSubmitQuestionResult);
+
+    if (questionSubmitStatusForUser === 'Allowed') {
+      throw new Error(
+        `${username} has rights for submitting questions!`);
+    } else {
+      showMessage(
+        `${username} doesn't have rights for submitting questions.`);
+    }
+  }
+
+  /**
+   * Function to check if the user is displayed as a question reviewer
+   * @param {string} username - the user expected to be displayed.
+   */
+  async verifyQuestionReviewersIncludeUser(username) {
+    const displayedUsers = await this.getDisplayedListOfQuestionReviewers();
+
+    if (!displayedUsers.includes(username)) {
+      throw new Error(
+        `${username} does not have rights for reviewing questions!`);
+    }
+  }
+
+  /**
+   * Function to check if the user is displayed as a question submitter
+   * @param {string} username - the user expected to be displayed.
+   */
+  async verifyQuestionSubmittersIncludeUser(username) {
+    const displayedUsers = await this.getDisplayedListOfQuestionSubmitters();
+
+    if (!displayedUsers.includes(username)) {
+      throw new Error(
+        `${username} does not have rights for submitting questions!`);
+    }
+  }
+
+  /**
+   * 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) {
+    const displayedUsers = await this.getDisplayedListOfQuestionReviewers();
+
+    if (displayedUsers.includes(username)) {
+      throw new Error(
+        `${username} has the right to review questions!`);
+    }
+  }
+
+  /**
+   * 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) {
+    const displayedUsers = await this.getDisplayedListOfQuestionSubmitters();
+
+    if (displayedUsers.includes(username)) {
+      throw new Error(
+        `${username} has the right to submit questions!`);
+    }
+  }
+};