From 4730edb0263135e88dab08ab0f6dc724a1585a67 Mon Sep 17 00:00:00 2001
From: RD Rama Devi <122200035+Rd4dev@users.noreply.github.com>
Date: Thu, 5 Sep 2024 22:52:56 +0530
Subject: [PATCH 1/5] Fix #1730 : Prevent binary files from being checked in
using pre-commit hook (#5525)
## Explanation
Fixes #1730
### The PR includes
- A pre-commit hook that identifies binary files in both
- staged changes (for local checks before committing)
- committed files (for CI checks after pushing to a PR)
- If binary files are found, the commit/check is blocked, and the
offending files are listed for removal.
- The pre-commit hook is integrated via a setup script.
- The same script is utilized for the CI pipeline.
- The 'Pass' statement is only included in the CI checks to keep the
local commit process cleaner.
### Local block as pre-commit hook when a binary file is detected
![image](https://github.com/user-attachments/assets/b953b2d7-55ec-46e6-a0b9-00967200509c)
### CI re-check if the PR includes a binary - [ Fail - [stack
trace](https://github.com/Rd4dev/oppia-android/actions/runs/10715972847/job/29712474511#step:7:15)
] [ Pass - [stack
trace](https://github.com/Rd4dev/oppia-android/actions/runs/10716040065/job/29712695824?pr=11#step:7:10)
]
![image](https://github.com/user-attachments/assets/64352473-86cb-49e0-b888-d3247286e9e5)
## Essential Checklist
- [x] The PR title and explanation each start with "Fix #bugnum: " (If
this PR fixes part of an issue, prefix the title with "Fix part of
#bugnum: ...".)
- [x] Any changes to
[scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets)
files have their rationale included in the PR explanation.
- [x] The PR follows the [style
guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide).
- [x] The PR does not contain any unnecessary code changes from Android
Studio
([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)).
- [x] The PR is made from a branch that's **not** called "develop" and
is up-to-date with "develop".
- [x] The PR is **assigned** to the appropriate reviewers
([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)).
---
.github/workflows/static_checks.yml | 11 +++++++++
scripts/pre-commit.sh | 35 +++++++++++++++++++++++++++++
scripts/setup.sh | 3 +++
3 files changed, 49 insertions(+)
create mode 100644 scripts/pre-commit.sh
diff --git a/.github/workflows/static_checks.yml b/.github/workflows/static_checks.yml
index 15dd3e28e76..ae04da9c0f4 100644
--- a/.github/workflows/static_checks.yml
+++ b/.github/workflows/static_checks.yml
@@ -107,6 +107,8 @@ jobs:
CACHE_DIRECTORY: ~/.bazel_cache
steps:
- uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
- name: Set up Bazel
uses: abhinavsingh/setup-bazel@v3
@@ -205,6 +207,15 @@ jobs:
run: |
bazel run //scripts:string_resource_validation_check -- $(pwd)
+ - name: Binary files check
+ # The expression if: ${{ !cancelled() }} runs a job or step regardless of its success or failure while responding to cancellations,
+ # serving as a cancellation-compliant alternative to if: ${{ always() }} in concurrent workflows.
+ if: ${{ !cancelled() }}
+ run: |
+ bash /home/runner/work/oppia-android/oppia-android/scripts/pre-commit.sh
+ echo "No binary files found in commit"
+ echo "BINARY FILES CHECK PASSED"
+
# Note that caching is intentionally not enabled for this check since licenses should always be
# verified without any potential influence from earlier builds (i.e. always from a clean build to
# ensure the results exactly match the current state of the repository).
diff --git a/scripts/pre-commit.sh b/scripts/pre-commit.sh
new file mode 100644
index 00000000000..26061ef5c09
--- /dev/null
+++ b/scripts/pre-commit.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+
+# Pre-commit hook to check for binary files.
+
+# Find the common ancestor between develop and the current branch
+base_commit=$(git merge-base 'origin/develop' HEAD)
+
+# Get the list of staged changes (files ready to be committed)
+staged_files=$(git diff --cached --name-only)
+
+# Get the list of changed files compared to the base commit
+changed_files=$(git diff --name-only "$base_commit" HEAD)
+
+# Combine both lists of files, ensuring no duplicates
+all_files=$(echo -e "$staged_files\n$changed_files" | sort -u)
+
+function checkForBinaries() {
+ binaryFilesCount=0
+
+ # Iterate over all files (both staged and changed)
+ for file in $all_files; do
+ if [ -f "$file" ] && file --mime "$file" | grep -q 'binary'; then
+ ((binaryFilesCount++))
+ printf "\n\033[33m%s\033[0m" "$file"
+ fi
+ done
+
+ if [[ "${binaryFilesCount}" -gt 0 ]]; then
+ printf "\n\nPlease remove the %d detected binary file(s)." "$binaryFilesCount"
+ printf "\nBINARY FILES CHECK FAILED"
+ exit 1
+ fi
+}
+
+checkForBinaries
diff --git a/scripts/setup.sh b/scripts/setup.sh
index 8c3ef595e1d..9f822095f32 100644
--- a/scripts/setup.sh
+++ b/scripts/setup.sh
@@ -13,6 +13,9 @@
# Move file from script folder to .git/hooks folder
cp scripts/pre-push.sh .git/hooks/pre-push
+# Copy the pre-commit hook from script to .git/hooks folder
+cp scripts/pre-commit.sh .git/hooks/pre-commit
+
# Create a folder where all the set up files will be downloaded
mkdir -p ../oppia-android-tools
cd ../oppia-android-tools
From 4f4831ac9d251930568369471efabea3175e9001 Mon Sep 17 00:00:00 2001
From: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com>
Date: Wed, 11 Sep 2024 02:17:46 +0300
Subject: [PATCH 2/5] Fix #5357: Remove CDATA from translatable strings (#5524)
## Explanation
Fixes https://github.com/oppia/oppia-android/issues/5357
This PR includes the changes previously included in #5361, with an
additional fix for missing closing html tags described in #5403:
```
faq_answer_app_language: Missing tag after the email address.
faq_answer_bug_reporting: Missing closing tag.
faq_answer_exploration_player: Missing closing tag.
faq_answer_update_app: Missing closing tag and closing
tag.
faq_answer_update_os: Missing closing tag and closing tag.
```
This PR removes all CDATA declarations in the translatable strings.xml
file and instead escapes all necessary characters: < and > (& didn't
need to be escaped since no strings use that character at the moment).
This is needed because Translatewiki doesn't seem to extract the HTML
within the CDATA declaration correctly, so it may not be translated
(some existing strings were never translated, and per
https://github.com/oppia/oppia-android/pull/5274 the latest FAQ changes
aren't being processed correctly (leading to empty translation strings
being submitted).
This PR also introduces a regex check + test to ensure that CDATA isn't
used anymore in strings.
## Essential Checklist
- [x] The PR title and explanation each start with "Fix #bugnum: " (If
this PR fixes part of an issue, prefix the title with "Fix part of
#bugnum: ...".)
- [x] Any changes to
[scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets)
files have their rationale included in the PR explanation.
- [x] The PR follows the [style
guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide).
- [x] The PR does not contain any unnecessary code changes from Android
Studio
([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)).
- [x] The PR is made from a branch that's **not** called "develop" and
is up-to-date with "develop".
- [x] The PR is **assigned** to the appropriate reviewers
([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)).
## For UI-specific PRs only
If your PR includes UI-related changes, then:
- Add screenshots for portrait/landscape for both a tablet & phone of
the before & after UI changes
- For the screenshots above, include both English and pseudo-localized
(RTL) screenshots (see [RTL
guide](https://github.com/oppia/oppia-android/wiki/RTL-Guidelines))
- Add a video showing the full UX flow with a screen reader enabled (see
[accessibility
guide](https://github.com/oppia/oppia-android/wiki/Accessibility-A11y-Guide))
- For PRs introducing new UI elements or color changes, both light and
dark mode screenshots must be included
- Add a screenshot demonstrating that you ran affected Espresso tests
locally & that they're passing
---
app/src/main/res/values-sw/strings.xml | 2 +-
app/src/main/res/values/strings.xml | 44 +++++++++----------
.../file_content_validation_checks.textproto | 5 +++
.../regex/RegexPatternValidationCheckTest.kt | 24 ++++++++++
4 files changed, 52 insertions(+), 23 deletions(-)
diff --git a/app/src/main/res/values-sw/strings.xml b/app/src/main/res/values-sw/strings.xml
index 049ebac057a..17d2fd54cec 100644
--- a/app/src/main/res/values-sw/strings.xml
+++ b/app/src/main/res/values-sw/strings.xml
@@ -152,7 +152,7 @@
Tafadhali anza jibu lako kwa nambari (k.m.,”0” katika 0.5).
Tafadhali weka nambari halali.
Jibu linaweza kuwa na tarakimu zisizozidi 15 (0–9) au alama (. au -).
- Tafadhali andika uwiano unaojumuisha tarakimu zilizotenganishwa na koloni(k.m. 1:2 au 1:2:3).
+ Tafadhali andika uwiano unaojumuisha tarakimu zilizotenganishwa na koloni (k.m. 1:2 au 1:2:3).
Tafadhali weka uwiano sahihi (k.m. 1:2 au 1:2:3).
Jibu lako lina koloni mbili (:) karibu na kila moja.
Idadi ya masharti si sawa na masharti yanayohitajika.
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 1395b1d24a3..319d70ff93a 100755
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -175,12 +175,12 @@
Please enter a valid number.
The answer can contain at most 15 digits (0–9) or symbols (. or -).
Enter a number to continue.
- Please write a ratio that consists of digits separated by colons (e.g. 1:2 or 1:2:3).
- Please enter a valid ratio (e.g. 1:2 or 1:2:3).
- Your answer has two colons (:) next to each other.
- Number of terms is not equal to the required terms.
- Ratios cannot have 0 as an element.
- Enter a ratio to continue.
+ Please write a ratio that consists of digits separated by colons (e.g. 1:2 or 1:2:3).
+ Please enter a valid ratio (e.g. 1:2 or 1:2:3).
+ Your answer has two colons (:) next to each other.
+ Number of terms is not equal to the required terms.
+ Ratios cannot have 0 as an element.
+ Enter a ratio to continue.
Enter text to continue.
Choose an answer to continue.
Unknown size
@@ -549,11 +549,11 @@
Policy Page
Privacy Policy
- this page for the latest version of this privacy policy.]]>
+ Please visit <a href="https://www.oppia.org/privacy-policy">this page</a> for the latest version of this privacy policy.
Terms of Service
- Terms of Service and Privacy Policy.]]>
- this page for the latest version of these terms.]]>
+ By using %s, you agree to our <br> <oppia-noninteractive-policy link="tos">Terms of Service</oppia-noninteractive-policy> and <oppia-noninteractive-policy link="privacy">Privacy Policy</oppia-noninteractive-policy>.
+ Please visit <a href="https://www.oppia.org/terms">this page</a> for the latest version of these terms.
What is %s?
Who is an Administrator?
@@ -569,19 +569,19 @@
How do I update my Android OS?
I can\'t find my question here. What now?
- %1$s "O-pee-yah" (Finnish) - "to learn"
%1$s\'s mission is to help anyone learn anything they want in an effective and enjoyable way.
By creating a set of free, high-quality, demonstrably effective lessons with the help of educators from around the world, %1$s aims to provide students with quality education — regardless of where they are or what traditional resources they have access to.
As a student, you can begin your learning adventure by browsing the topics listed on the Home Page!
]]>
- An Administrator is the main user that manages profiles and settings for every profile on their account. They are most likely your parent, teacher, or guardian that created this profile for you.
Administrators have the ability to manage profiles, assign PINs, and change other settings under their account. Depending on your profile, Administrator permissions may be required for certain features such as changing your PIN, and more.
To see who your Administrator is, go to the Profile Chooser. The first profile listed and has "Administrator" written under their name is the Administrator.
]]>
- If it is your first time creating a profile and you do not have a PIN:- From the Profile Chooser, tap on Set up Multiple Profiles.
- Create a PIN and Save.
- Fill in all fields for the profile.
- (Optional) Upload a photo.
- Enter a name.
- (Optional) Assign a 3-digit PIN.
- Tap Create. This profile is added to your Profile Chooser!
If you have created a profile before and have a PIN:
- From the Profile Chooser, tap on Add Profile.
- Enter your PIN and tap Submit.
- Fill in all fields for the profile.
- (Optional) Upload a photo.
- Enter a name.
- (Optional) Assign a 3-digit PIN.
- Tap Create. This profile is added to your Profile Chooser!
Note: Only the Administrator is able to manage profiles.
]]>
- The %s app currently supports English, Brazilian Portuguese, Arabic, Swahili and Nigerian Pidgin. Choose one of these languages in the menu, under Options. To request the app in your language, please contact us at admin@oppia.org.]]>
- - From your %s app home screen, tap the menu in the top left corner.
- Tap Share feedback.
- Follow the instructions to report the bug or share feedback.
]]>
- %1$s’s mission is to help learners gain necessary life skills. Math is an essential skill in everyday life. %1$s will be offering new lessons on science and other subjects soon!]]>
- Yes, %s will be offering new lessons on science and other subjects soon. Please check back for updates!]]>
- If the Exploration Player is not loading
Check to see if the app is up to date:
- Go to the Play Store and make sure the app is updated to its latest version
Check your internet connection:
- If your internet connection is slow, try re-connecting to your Wi-Fi network or connecting to a different network.
Ask the Administrator to check their device and internet connection:
- Get the Administrator to troubleshoot using the steps above
Let us know if you still have issues with loading:
- Report a problem by contacting us at admin@oppia.org.
]]>
- If your audio is not playing
Check to see if the app is up to date:
- Go to the Play Store and make sure the app is updated to its latest version
Check your internet connection:
- If your internet connection is slow, try re-connecting to your Wi-Fi network or connecting to a different network. Slow internet may cause the audio to load irregularly, making it difficult to play.
Ask the Administrator to check their device and internet connection:
- Get the Administrator to troubleshoot using the steps above
Let us know if you still have issues with loading:
- Report a problem by contacting us at admin@oppia.org.
]]>
- Once a profile is deleted:- The profile cannot be recovered.
- Profile information such as name, photos, and progress will be permanently deleted.
To delete a profile (excluding the Administrator\'s):
- From the Administrator\'s Home Page, tap on the menu button on the top left.
- Tap on Administrator Controls.
- Tap on Edit Profiles.
- Tap on the Profile you would like to delete.
- At the bottom of the screen, tap Profile Deletion.
- Tap Delete to confirm deletion.
Note: Only the Administrator is able to manage profiles.
]]>
- - Open the Google Play Store app.
- Search for the %s app.
- Tap Update.]]>
- - Tap your phone\'s Settings app.
- Tap System updates.
- Tap System updates and follow the instructions to update your Android operating system.]]>
- If you cannot find your question or would like to report a bug, contact us at admin@oppia.org.]]>
+ <p>%1$s <i>"O-pee-yah"</i> (Finnish) - "to learn"</p><p><br></p><p>%1$s\'s mission is to help anyone learn anything they want in an effective and enjoyable way.</p><p><br></p><p>By creating a set of free, high-quality, demonstrably effective lessons with the help of educators from around the world, %1$s aims to provide students with quality education — regardless of where they are or what traditional resources they have access to.</p><p><br></p><p>As a student, you can begin your learning adventure by browsing the topics listed on the Home Page!</p>
+ <p>An Administrator is the main user that manages profiles and settings for every profile on their account. They are most likely your parent, teacher, or guardian that created this profile for you.</p><p><br></p><p>Administrators have the ability to manage profiles, assign PINs, and change other settings under their account. Depending on your profile, Administrator permissions may be required for certain features such as changing your PIN, and more.</p><p><br></p><p>To see who your Administrator is, go to the Profile Chooser. The first profile listed and has "Administrator" written under their name is the Administrator.</p>
+ <p>If it is your first time creating a profile and you do not have a PIN:<ol><li>From the Profile Chooser, tap on <strong>Set up Multiple Profiles</strong>.</li><li>Create a PIN and <strong>Save</strong>.</li><li>Fill in all fields for the profile.<ol><li>(Optional) Upload a photo.</li><li>Enter a name.</li><li>(Optional) Assign a 3-digit PIN.</li></ol></li><li>Tap <strong>Create</strong>. This profile is added to your Profile Chooser!</li></ol></p><p>If you have created a profile before and have a PIN:<ol><li>From the Profile Chooser, tap on <strong>Add Profile</strong>.</li><li>Enter your PIN and tap <strong>Submit</strong>.</li><li>Fill in all fields for the profile.<ol><li>(Optional) Upload a photo.</li><li>Enter a name.</li><li>(Optional) Assign a 3-digit PIN.</li></ol></li><li>Tap <strong>Create</strong>. This profile is added to your Profile Chooser!</li></ol></p><p><br></p><p>Note: Only the <u>Administrator</u> is able to manage profiles.</p>
+ <p>The %s app currently supports English, Brazilian Portuguese, Arabic, Swahili, and Nigerian Pidgin. Choose one of these languages in the menu, under Options. To request the app in your language, please contact us at <strong>admin@oppia.org</strong>.</p>
+ <p><ol><li>From your %s app home screen, tap the menu in the top left corner.</li><li>Tap <strong>Share feedback</strong>.</li><li>Follow the instructions to report the bug or share feedback.</li></ol></p>
+ <p>%1$s\'s mission is to help learners gain necessary life skills. Math is an essential skill in everyday life. %1$s will be offering new lessons on science and other subjects soon!</p>
+ <p>Yes, %s will be offering new lessons on science and other subjects soon. Please check back for updates!</p>
+ <p>If the Exploration Player is not loading</p><p><br></p><p>Check to see if the app is up to date:</p><ul><li>Go to the Play Store and make sure the app is updated to its latest version</li></ul><p><br></p><p>Check your internet connection:</p><ul><li>If your internet connection is slow, try re-connecting to your Wi-Fi network or connecting to a different network.</li></ul><p>Ask the Administrator to check their device and internet connection:</p><ul><li>Get the Administrator to troubleshoot using the steps above</li></ul><p><br></p><p>Let us know if you still have issues with loading:</p><ul><li>Report a problem by contacting us at admin@oppia.org.</li></ul>
+ <p>If your audio is not playing</p><p><br></p><p>Check to see if the app is up to date:</p><ul><li>Go to the Play Store and make sure the app is updated to its latest version</li></ul><p><br></p><p>Check your internet connection:</p><ul><li>If your internet connection is slow, try re-connecting to your Wi-Fi network or connecting to a different network. Slow internet may cause the audio to load irregularly, making it difficult to play.</li></ul><p><br></p><p>Ask the Administrator to check their device and internet connection:</p><ul><li>Get the Administrator to troubleshoot using the steps above</li></ul><p><br></p><p>Let us know if you still have issues with loading:</p><ul><li>Report a problem by contacting us at admin@oppia.org.</li></ul>
+ <p>Once a profile is deleted:</p><ol><li>The profile cannot be recovered.</li><li>Profile information such as name, photos, and progress will be permanently deleted.</li></ol><p>To delete a profile (excluding the <u>Administrator</u>):</p><ol><li>From the Administrator\'s Home Page, tap on the menu button on the top left.</li><li>Tap on <strong>Administrator Controls</strong>.</li><li>Tap on <strong>Edit Profiles</strong>.</li><li>Tap on the Profile you would like to delete.</li><li>At the bottom of the screen, tap <strong>Profile Deletion</strong>.</li><li>Tap <strong>Delete</strong> to confirm deletion.</li></ol><p><br></p><p>Note: Only the <u>Administrator</u> is able to manage profiles.</p>
+ <p><ol><li>Open the Google Play Store app.</li><li>Search for the %s app.</li><li>Tap Update.</li></ol></p>
+ <p><ol><li>Tap your phone\'s Settings app.</li><li>Tap System updates.</li><li>Tap System updates and follow the instructions to update your Android operating system.</li></ol></p>
+ <p>If you cannot find your question or would like to report a bug, contact us at <strong>admin@oppia.org.</strong></p>
Profile Edit Fragment Test Activity
Administrator Controls Fragment Test Activity
diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto
index 9a9918f2f64..d4e3597a664 100644
--- a/scripts/assets/file_content_validation_checks.textproto
+++ b/scripts/assets/file_content_validation_checks.textproto
@@ -116,6 +116,11 @@ file_content_checks {
failure_message: "All plurals outside strings.xml must be marked as not translatable, or moved to strings.xml."
exempted_file_patterns: "app/src/main/res/values.*?/strings\\.xml"
}
+file_content_checks {
+ file_path_regex: "app/src/main/res/values/strings\\.xml"
+ prohibited_content_regex: "CDATA"
+ failure_message: "CDATA isn't handled by Translatewiki correctly. Use escaped HTML, instead."
+}
file_content_checks {
file_path_regex: ".+?\\.kt"
prohibited_content_regex: "android.text.BidiFormatter"
diff --git a/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt b/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt
index 4e96123650e..5f47d4b81e1 100644
--- a/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt
+++ b/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt
@@ -222,6 +222,8 @@ class RegexPatternValidationCheckTest {
private val referenceComputeIfAbsent =
"computeIfAbsent won't desugar and requires Java 8 support (SDK 24+). Suggest using an atomic" +
" Kotlin-specific solution, instead."
+ private val cdataShouldNotBeUsed =
+ "CDATA isn't handled by Translatewiki correctly. Use escaped HTML, instead."
private val wikiReferenceNote =
"Refer to https://github.com/oppia/oppia-android/wiki/Static-Analysis-Checks" +
"#regexpatternvalidation-check for more details on how to fix this."
@@ -2752,6 +2754,28 @@ class RegexPatternValidationCheckTest {
)
}
+ @Test
+ fun testFileContent_includesCdataContentInStringsXml_fileContentIsNotCorrect() {
+ val prohibitedContent =
+ """
+ Some nested HTML.]]>
+ """.trimIndent()
+ tempFolder.newFolder("testfiles", "app", "src", "main", "res", "values")
+ val stringFilePath = "app/src/main/res/values/strings.xml"
+ tempFolder.newFile("testfiles/$stringFilePath").writeText(prohibitedContent)
+
+ val exception = assertThrows() { runScript() }
+
+ assertThat(exception).hasMessageThat().contains(REGEX_CHECK_FAILED_OUTPUT_INDICATOR)
+ assertThat(outContent.toString().trim())
+ .isEqualTo(
+ """
+ $stringFilePath:1: $cdataShouldNotBeUsed
+ $wikiReferenceNote
+ """.trimIndent()
+ )
+ }
+
/** Runs the regex_pattern_validation_check. */
private fun runScript() {
main(File(tempFolder.root, "testfiles").absolutePath)
From c82b71b82717e3c998fbdb7bfb052bd580d6d78e Mon Sep 17 00:00:00 2001
From: RD Rama Devi <122200035+Rd4dev@users.noreply.github.com>
Date: Thu, 12 Sep 2024 15:05:07 +0530
Subject: [PATCH 3/5] Fix part of #5508: Limit APK/AAB Difference analysis
reports in the PR Comment Thread (#5532)
## Explanation
Fixes part of #5508
### This PR includes
Steps to locate the previous `stats.yml` workflow run, download its
build artifact, and compare it with the current build log. If changes
are detected, a comment will be uploaded to help minimize comment thread
overload.
**The implementation:**
- Download the previous build log artifact (if available).
- Run the script.
- Compare the current generated build log with the previous build log
artifact:
- if no differences are found -> skip commenting.
- if differences are found -> comment the current generated build log
- if no previous artifact is found -> comment the current generated
build log
- This occurs in 2 instances:
- 1. It's the first run of the PR.
- 2. An error occurred during the previous stat check (since the
previous build is from the second-to-last run ID).
- Upload the current build log as an artifact (for the next stat run).
- Comment/skip the stat report based on the comparison result.
#
### Tested with a cloned PR
_(with stats.yml implementation on develop)_
Tested PR:
https://github.com/Rd4dev/Oppia-Android-Fork-from-Fork/pull/40
Reference for proof of implementation:
- [x] should comment on initial run |
[comment](https://github.com/Rd4dev/Oppia-Android-Fork-from-Fork/pull/40#issuecomment-2336828924)
| [stack
trace](https://github.com/Rd4dev/Oppia-Android-Fork-from-Fork/actions/runs/10762876260/job/29843752198#step:19:26)
- [x] shouldn't comment when no change |
[reference1](https://github.com/Rd4dev/Oppia-Android-Fork-from-Fork/pull/40#issuecomment-2337237045)
|
[reference2](https://github.com/Rd4dev/Oppia-Android-Fork-from-Fork/pull/40#issuecomment-2337278404)
- [x] should comment on change |
[comment](https://github.com/Rd4dev/Oppia-Android-Fork-from-Fork/pull/40#issuecomment-2337251971)
|
[reference](https://github.com/Rd4dev/Oppia-Android-Fork-from-Fork/pull/40#issuecomment-2337260315)
- [x] comment on previous build fail (replicated!) |
[comment](https://github.com/Rd4dev/Oppia-Android-Fork-from-Fork/pull/40#issuecomment-2337297618)
|
[reference](https://github.com/Rd4dev/Oppia-Android-Fork-from-Fork/pull/40#issuecomment-2337310429)
## Essential Checklist
- [x] The PR title and explanation each start with "Fix #bugnum: " (If
this PR fixes part of an issue, prefix the title with "Fix part of
#bugnum: ...".)
- [x] Any changes to
[scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets)
files have their rationale included in the PR explanation.
- [x] The PR follows the [style
guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide).
- [x] The PR does not contain any unnecessary code changes from Android
Studio
([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)).
- [x] The PR is made from a branch that's **not** called "develop" and
is up-to-date with "develop".
- [x] The PR is **assigned** to the appropriate reviewers
([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)).
---------
Co-authored-by: Ben Henning
---
.github/workflows/build_tests.yml | 2 +-
.github/workflows/main.yml | 10 +++---
.github/workflows/stats.yml | 60 ++++++++++++++++++++++++++++++-
3 files changed, 65 insertions(+), 7 deletions(-)
diff --git a/.github/workflows/build_tests.yml b/.github/workflows/build_tests.yml
index 7dde621a72f..7a024172368 100644
--- a/.github/workflows/build_tests.yml
+++ b/.github/workflows/build_tests.yml
@@ -153,7 +153,7 @@ jobs:
run: |
cp $GITHUB_WORKSPACE/bazel-bin/oppia.apk /home/runner/work/oppia-android/oppia-android/
- - uses: actions/upload-artifact@v2
+ - uses: actions/upload-artifact@v4
with:
name: oppia-bazel.apk
path: /home/runner/work/oppia-android/oppia-android/oppia.apk
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 84cc12006a8..f5ba874970e 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -64,7 +64,7 @@ jobs:
# We require 'sudo' to avoid an error of the existing android sdk. See https://github.com/actions/starter-workflows/issues/58
run: sudo ./gradlew --full-stacktrace :utility:testDebugUnitTest -Dorg.gradle.java.home=$JAVA_HOME
- name: Upload Utility Test Reports
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v4
if: ${{ !cancelled() }} # IMPORTANT: Upload reports regardless of success or failure status
with:
name: utility reports
@@ -77,7 +77,7 @@ jobs:
# We require 'sudo' to avoid an error of the existing android sdk. See https://github.com/actions/starter-workflows/issues/58
run: sudo ./gradlew --full-stacktrace :data:testDebugUnitTest -Dorg.gradle.java.home=$JAVA_HOME
- name: Upload Data Test Reports
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v4
if: ${{ !cancelled() }} # IMPORTANT: Upload reports regardless of success or failure status
with:
name: data reports
@@ -90,7 +90,7 @@ jobs:
# We require 'sudo' to avoid an error of the existing android sdk. See https://github.com/actions/starter-workflows/issues/58
run: sudo ./gradlew --full-stacktrace :domain:testDebugUnitTest -Dorg.gradle.java.home=$JAVA_HOME
- name: Upload Domain Test Reports
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v4
if: ${{ !cancelled() }} # IMPORTANT: Upload reports regardless of success or failure status
with:
name: domain reports
@@ -103,7 +103,7 @@ jobs:
# We require 'sudo' to avoid an error of the existing android sdk. See https://github.com/actions/starter-workflows/issues/58
run: sudo ./gradlew --full-stacktrace :testing:testDebugUnitTest -Dorg.gradle.java.home=$JAVA_HOME
- name: Upload Testing Test Reports
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v4
if: ${{ !cancelled() }} # IMPORTANT: Upload reports regardless of success or failure status
with:
name: testing reports
@@ -144,7 +144,7 @@ jobs:
run: |
sudo ./gradlew --full-stacktrace :app:testDebugUnitTest --${{ matrix.shard }} -Dorg.gradle.java.home=$JAVA_HOME
- name: Upload App Test Reports
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v4
if: ${{ !cancelled() }} # IMPORTANT: Upload reports regardless of success or failure status
with:
name: app reports ${{ matrix.shard }}
diff --git a/.github/workflows/stats.yml b/.github/workflows/stats.yml
index de3ce5fadd5..74cbbeabbed 100644
--- a/.github/workflows/stats.yml
+++ b/.github/workflows/stats.yml
@@ -184,6 +184,63 @@ jobs:
beta $(pwd)/oppia_beta_without_changes.aab $(pwd)/oppia_beta_with_changes.aab \
ga $(pwd)/oppia_ga_without_changes.aab $(pwd)/oppia_ga_with_changes.aab
+ - name: Find CI workflow run for PR
+ id: find-workflow-run
+ uses: actions/github-script@v7
+ continue-on-error: true
+ with:
+ script: |
+ const { owner, repo } = context.repo;
+ const runsResponse = await github.rest.actions.listWorkflowRuns({
+ owner,
+ repo,
+ workflow_id: 'stats.yml',
+ });
+
+ const runs = runsResponse.data.workflow_runs;
+ runs.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
+
+ const run = runs[1];
+ if(!run) {
+ core.setFailed('Could not find a succesful workflow run');
+ return;
+ }
+ console.log(run.id);
+
+ core.setOutput('run-id', run.id);
+
+ - name: Download previous build summary
+ uses: actions/download-artifact@v4
+ with:
+ name: brief_build_summary_${{ matrix.prInfo.number }}
+ path: ./previous_build_logs
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ run-id: ${{ steps.find-workflow-run.outputs.run-id }}
+ continue-on-error: true # Ignore errors if the file doesn't exist (first run)
+
+ - name: Compare current build summary with the previous one
+ id: build-comparison
+ run: |
+ if [ -f ./develop/brief_build_summary.log ]; then
+ echo "Comparing current and previous build summaries..."
+ if diff ./develop/brief_build_summary.log ./previous_build_logs/brief_build_summary.log > /dev/null; then
+ echo "No changes detected; skipping comment."
+ echo "skip_comment=true" >> $GITHUB_ENV
+ else
+ echo "Changes detected; proceeding with the comment."
+ echo "skip_comment=false" >> $GITHUB_ENV
+ fi
+ else
+ echo "No previous summary found; proceeding with the comment."
+ echo "skip_comment=false" >> $GITHUB_ENV
+ fi
+
+ - name: Upload current build summary for future comparison
+ uses: actions/upload-artifact@v4
+ with:
+ name: brief_build_summary_${{ matrix.prInfo.number }}
+ path: ./develop/brief_build_summary.log
+
# Reference: https://github.com/peter-evans/create-or-update-comment#setting-the-comment-body-from-a-file.
# Also, for multi-line env values, see: https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings.
- name: Extract reports for uploading & commenting
@@ -203,6 +260,7 @@ jobs:
cp "$GITHUB_WORKSPACE/develop/full_build_summary.log" "$FULL_BUILD_SUMMARY_FILE_PATH"
- name: Add build stats summary comment
+ if: ${{ env.skip_comment == 'false' }}
env:
PR_NUMBER: ${{ matrix.prInfo.number }}
uses: peter-evans/create-or-update-comment@v1
@@ -210,7 +268,7 @@ jobs:
issue-number: ${{ env.PR_NUMBER }}
body: ${{ steps.compute-comment-body.outputs.comment_body }}
- - uses: actions/upload-artifact@v2
+ - uses: actions/upload-artifact@v4
with:
name: ${{ env.FULL_BUILD_SUMMARY_FILE_NAME }}
path: ${{ env.FULL_BUILD_SUMMARY_FILE_PATH }}
From 65197bfad9a11db12838222b4050f6818fe87b8c Mon Sep 17 00:00:00 2001
From: Subhajit Mallick <153619690+subhajitxyz@users.noreply.github.com>
Date: Mon, 16 Sep 2024 16:04:19 +0530
Subject: [PATCH 4/5] Fix #5485 Create means for verifying Fragment Arguments
(#5527)
## Explanation
Fixes #5485
Added test for 10 fragments arguments and saveInstanceState.
## Essential Checklist
- [x] The PR title and explanation each start with "Fix #bugnum: " (If
this PR fixes part of an issue, prefix the title with "Fix part of
#bugnum: ...".)
- [ ] Any changes to
[scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets)
files have their rationale included in the PR explanation.
- [x] The PR follows the [style
guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide).
- [x] The PR does not contain any unnecessary code changes from Android
Studio
([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)).
- [x] The PR is made from a branch that's **not** called "develop" and
is up-to-date with "develop".
- [x] The PR is **assigned** to the appropriate reviewers
([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)).
## For UI-specific PRs only
If your PR includes UI-related changes, then:
- Add screenshots for portrait/landscape for both a tablet & phone of
the before & after UI changes
- For the screenshots above, include both English and pseudo-localized
(RTL) screenshots (see [RTL
guide](https://github.com/oppia/oppia-android/wiki/RTL-Guidelines))
- Add a video showing the full UX flow with a screen reader enabled (see
[accessibility
guide](https://github.com/oppia/oppia-android/wiki/Accessibility-A11y-Guide))
- For PRs introducing new UI elements or color changes, both light and
dark mode screenshots must be included
- Add a screenshot demonstrating that you ran affected Espresso tests
locally & that they're passing
---------
Co-authored-by: Mr. 17
---
.../app/options/ReadingTextSizeFragment.kt | 3 +-
.../options/ReadingTextSizeFragmentTest.kt | 43 ++++++++++++
.../resumelesson/ResumeLessonFragmentTest.kt | 37 ++++++++++
.../profile/ProfileListFragmentTest.kt | 27 ++++++++
.../profile/ProfileRenameFragmentTest.kt | 26 +++++++
.../profile/ProfileResetPinFragmentTest.kt | 67 +++++++++++++++++++
.../app/spotlight/SpotlightFragmentTest.kt | 21 ++++++
.../android/app/story/StoryFragmentTest.kt | 42 ++++++++++++
.../android/app/survey/SurveyFragmentTest.kt | 29 ++++++++
.../ThirdPartyDependencyListFragmentTest.kt | 28 ++++++++
.../revisioncard/RevisionCardFragmentTest.kt | 43 ++++++++++++
11 files changed, 365 insertions(+), 1 deletion(-)
diff --git a/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeFragment.kt b/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeFragment.kt
index 1cdae8c8578..02c9a456e74 100644
--- a/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeFragment.kt
+++ b/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeFragment.kt
@@ -63,7 +63,8 @@ class ReadingTextSizeFragment : InjectableFragment(), TextSizeRadioButtonListene
readingTextSizeFragmentPresenter.onTextSizeSelected(selectedTextSize)
}
- private fun retrieveFragmentArguments(): ReadingTextSizeFragmentArguments {
+ /** Returns the [ReadingTextSizeFragmentArguments] stored in the fragment's arguments. */
+ fun retrieveFragmentArguments(): ReadingTextSizeFragmentArguments {
return checkNotNull(arguments) {
"Expected arguments to be passed to ReadingTextSizeFragment"
}.getProto(FRAGMENT_ARGUMENTS_KEY, ReadingTextSizeFragmentArguments.getDefaultInstance())
diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt
index 5e4b14be4aa..1883d2459bf 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt
@@ -16,6 +16,7 @@ import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
import dagger.Component
import org.hamcrest.Description
import org.hamcrest.TypeSafeMatcher
@@ -37,6 +38,7 @@ import org.oppia.android.app.application.testing.TestingBuildFlavorModule
import org.oppia.android.app.devoptions.DeveloperOptionsModule
import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule
import org.oppia.android.app.model.ProfileId
+import org.oppia.android.app.model.ReadingTextSize.MEDIUM_TEXT_SIZE
import org.oppia.android.app.model.ReadingTextSize.SMALL_TEXT_SIZE
import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule
import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView
@@ -194,6 +196,47 @@ class ReadingTextSizeFragmentTest {
}
}
+ @Test
+ fun testFragment_fragmentLoaded_verifyCorrectArgumentsPassed() {
+ launch(createReadingTextSizeActivityIntent()).use { scenario ->
+ testCoroutineDispatchers.runCurrent()
+ scenario.onActivity { activity ->
+
+ val readingTextSizeFragment = activity.supportFragmentManager
+ .findFragmentById(R.id.reading_text_size_container) as ReadingTextSizeFragment
+ val receivedReadingTextSize = readingTextSizeFragment.retrieveFragmentArguments()
+ .readingTextSize
+
+ assertThat(receivedReadingTextSize).isEqualTo(SMALL_TEXT_SIZE)
+ }
+ }
+ }
+
+ @Test
+ fun testFragment_saveInstanceState_verifyCorrectStateRestored() {
+ launch(createReadingTextSizeActivityIntent()).use { scenario ->
+ testCoroutineDispatchers.runCurrent()
+
+ scenario.onActivity { activity ->
+ val readingTextSizeFragment = activity.supportFragmentManager
+ .findFragmentById(R.id.reading_text_size_container) as ReadingTextSizeFragment
+ readingTextSizeFragment.readingTextSizeFragmentPresenter
+ .onTextSizeSelected(MEDIUM_TEXT_SIZE)
+ }
+
+ scenario.recreate()
+
+ scenario.onActivity { activity ->
+ val newReadingTextSizeFragment = activity.supportFragmentManager
+ .findFragmentById(R.id.reading_text_size_container) as ReadingTextSizeFragment
+ val restoredTopicIdList =
+ newReadingTextSizeFragment.readingTextSizeFragmentPresenter.getTextSizeSelected()
+
+ assertThat(restoredTopicIdList).isEqualTo(MEDIUM_TEXT_SIZE)
+ }
+ }
+ }
+
private fun createReadingTextSizeActivityIntent() =
ReadingTextSizeActivity.createReadingTextSizeActivityIntent(context, SMALL_TEXT_SIZE)
diff --git a/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentTest.kt
index 1a28e628594..33da1eeed36 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentTest.kt
@@ -41,6 +41,7 @@ import org.oppia.android.app.model.ExplorationActivityParams
import org.oppia.android.app.model.ExplorationCheckpoint
import org.oppia.android.app.model.ProfileId
import org.oppia.android.app.model.ReadingTextSize
+import org.oppia.android.app.model.ResumeLessonFragmentArguments
import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule
import org.oppia.android.app.shim.ViewBindingShimModule
import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule
@@ -99,6 +100,7 @@ import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
+import org.oppia.android.util.extensions.getProto
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.locale.LocaleProdModule
import org.oppia.android.util.logging.EventLoggingConfigurationModule
@@ -273,6 +275,41 @@ class ResumeLessonFragmentTest {
}
}
+ @Test
+ fun testFragment_fragmentLoaded_verifyCorrectArgumentsPassed() {
+ launch(createResumeLessonActivityIntent()).use { scenario ->
+ testCoroutineDispatchers.runCurrent()
+ scenario.onActivity { activity ->
+
+ val resumeLessonFragment = activity.supportFragmentManager
+ .findFragmentById(R.id.resume_lesson_fragment_placeholder) as ResumeLessonFragment
+ val args = checkNotNull(resumeLessonFragment.arguments) {
+ "Expected arguments to be provided for fragment."
+ }.getProto(
+ ResumeLessonFragment.RESUME_LESSON_FRAGMENT_ARGUMENTS_KEY,
+ ResumeLessonFragmentArguments.getDefaultInstance()
+ )
+ val receivedProfileId = args.profileId
+ val receivedClassroomId = args.classroomId
+ val receivedTopicId = args.topicId
+ val receivedStoryId = args.storyId
+ val receivedExplorationId = args.explorationId
+ val receivedParentScreen = args.parentScreen
+ val receivedCheckpoint = args.checkpoint
+
+ assertThat(receivedProfileId)
+ .isEqualTo(ProfileId.newBuilder().apply { internalId = 1 }.build())
+ assertThat(receivedClassroomId).isEqualTo(TEST_CLASSROOM_ID_1)
+ assertThat(receivedTopicId).isEqualTo(FRACTIONS_TOPIC_ID)
+ assertThat(receivedStoryId).isEqualTo(FRACTIONS_STORY_ID_0)
+ assertThat(receivedExplorationId).isEqualTo(FRACTIONS_EXPLORATION_ID_0)
+ assertThat(receivedParentScreen)
+ .isEqualTo(ExplorationActivityParams.ParentScreen.PARENT_SCREEN_UNSPECIFIED)
+ assertThat(receivedCheckpoint).isEqualTo(ExplorationCheckpoint.getDefaultInstance())
+ }
+ }
+ }
+
private fun createResumeLessonActivityIntent(): Intent {
return ResumeLessonActivity.createResumeLessonActivityIntent(
context,
diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListFragmentTest.kt
index 5b7bd78bb3e..742c20bd0f5 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListFragmentTest.kt
@@ -18,6 +18,7 @@ import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
import dagger.Component
import org.hamcrest.Matchers.not
import org.junit.After
@@ -37,6 +38,7 @@ import org.oppia.android.app.application.ApplicationStartupListenerModule
import org.oppia.android.app.application.testing.TestingBuildFlavorModule
import org.oppia.android.app.devoptions.DeveloperOptionsModule
import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule
+import org.oppia.android.app.model.ProfileListFragmentArguments
import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule
import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPosition
import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView
@@ -86,6 +88,7 @@ import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
+import org.oppia.android.util.extensions.getProto
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.locale.LocaleProdModule
import org.oppia.android.util.logging.EventLoggingConfigurationModule
@@ -367,6 +370,30 @@ class ProfileListFragmentTest {
}
}
+ @Test
+ fun testFragment_fragmentLoaded_verifyCorrectArgumentsPassed() {
+ profileTestHelper.initializeProfiles()
+ launch(ProfileListActivity::class.java).use { scenario ->
+ testCoroutineDispatchers.runCurrent()
+ scenario.onActivity { activity ->
+
+ val profileListFragment = activity.supportFragmentManager
+ .findFragmentById(R.id.profile_list_container) as ProfileListFragment
+
+ val arguments = checkNotNull(profileListFragment.arguments) {
+ "Expected variables to be passed to ProfileListFragment"
+ }
+ val args = arguments.getProto(
+ ProfileListFragment.PROFILE_LIST_FRAGMENT_ARGUMENTS_KEY,
+ ProfileListFragmentArguments.getDefaultInstance()
+ )
+ val receivedIsMultipane = args.isMultipane
+
+ assertThat(receivedIsMultipane).isEqualTo(false)
+ }
+ }
+ }
+
// TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them.
@Singleton
@Component(
diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentTest.kt
index fd4bf50712b..1fc5d9316d3 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentTest.kt
@@ -21,6 +21,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.ActivityTestRule
+import com.google.common.truth.Truth.assertThat
import dagger.Component
import org.hamcrest.CoreMatchers.not
import org.hamcrest.core.AllOf.allOf
@@ -102,6 +103,7 @@ import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule
import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule
import org.oppia.android.util.parser.image.GlideImageLoaderModule
import org.oppia.android.util.parser.image.ImageParsingModule
+import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
import javax.inject.Inject
@@ -441,6 +443,30 @@ class ProfileRenameFragmentTest {
}
}
+ @Test
+ fun testFragment_fragmentLoaded_verifyCorrectArgumentsPassed() {
+ ActivityScenario.launch(
+ ProfileRenameActivity.createProfileRenameActivity(
+ context = context,
+ internalProfileId = 1
+ )
+ ).use { scenario ->
+ testCoroutineDispatchers.runCurrent()
+ scenario.onActivity { activity ->
+
+ val profileRenameFragment = activity.supportFragmentManager
+ .findFragmentById(R.id.profile_rename_fragment_placeholder) as ProfileRenameFragment
+ val args =
+ checkNotNull(profileRenameFragment.arguments) {
+ "Expected arguments to be passed to ProfileRenameFragment"
+ }
+ val receivedProfileId = args.extractCurrentUserProfileId().internalId
+
+ assertThat(receivedProfileId).isEqualTo(1)
+ }
+ }
+ }
+
// TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them.
@Singleton
@Component(
diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentTest.kt
index c306e9a6206..c0e1541d8a3 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentTest.kt
@@ -22,6 +22,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.ActivityTestRule
+import com.google.common.truth.Truth.assertThat
import dagger.Component
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not
@@ -42,6 +43,7 @@ import org.oppia.android.app.application.ApplicationStartupListenerModule
import org.oppia.android.app.application.testing.TestingBuildFlavorModule
import org.oppia.android.app.devoptions.DeveloperOptionsModule
import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule
+import org.oppia.android.app.model.ProfileResetPinFragmentArguments
import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule
import org.oppia.android.app.shim.ViewBindingShimModule
import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule
@@ -92,6 +94,7 @@ import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
+import org.oppia.android.util.extensions.getProto
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.locale.LocaleProdModule
import org.oppia.android.util.logging.EventLoggingConfigurationModule
@@ -1006,6 +1009,70 @@ class ProfileResetPinFragmentTest {
}
}
+ @Test
+ fun testFragment_fragmentLoaded_verifyCorrectArgumentsPassed() {
+ ActivityScenario.launch(
+ ProfileResetPinActivity.createProfileResetPinActivity(
+ context = context,
+ profileId = 0,
+ isAdmin = true
+ )
+ ).use { scenario ->
+ testCoroutineDispatchers.runCurrent()
+ scenario.onActivity { activity ->
+
+ val profileResetPinFragment = activity.supportFragmentManager
+ .findFragmentById(R.id.profile_reset_pin_fragment_placeholder) as ProfileResetPinFragment
+
+ val arguments = checkNotNull(profileResetPinFragment.arguments) {
+ "Expected arguments to be passed to ProfileResetPinFragment"
+ }
+ val args =
+ arguments.getProto(
+ ProfileResetPinFragment.PROFILE_RESET_PIN_FRAGMENT_ARGUMENTS_KEY,
+ ProfileResetPinFragmentArguments.getDefaultInstance()
+ )
+ val receivedProfileResetPinProfileId = args.internalProfileId
+ val receivedProfileResetPinIsAdmin = args.isAdmin
+
+ assertThat(receivedProfileResetPinProfileId).isEqualTo(0)
+ assertThat(receivedProfileResetPinIsAdmin).isEqualTo(true)
+ }
+ }
+ }
+
+ @Test
+ fun testFragment_fragmentLoaded_whenIsAdminFalse_verifyCorrectArgumentsPassed() {
+ ActivityScenario.launch(
+ ProfileResetPinActivity.createProfileResetPinActivity(
+ context = context,
+ profileId = 0,
+ isAdmin = false
+ )
+ ).use { scenario ->
+ testCoroutineDispatchers.runCurrent()
+ scenario.onActivity { activity ->
+
+ val profileResetPinFragment = activity.supportFragmentManager
+ .findFragmentById(R.id.profile_reset_pin_fragment_placeholder) as ProfileResetPinFragment
+
+ val arguments = checkNotNull(profileResetPinFragment.arguments) {
+ "Expected arguments to be passed to ProfileResetPinFragment"
+ }
+ val args =
+ arguments.getProto(
+ ProfileResetPinFragment.PROFILE_RESET_PIN_FRAGMENT_ARGUMENTS_KEY,
+ ProfileResetPinFragmentArguments.getDefaultInstance()
+ )
+ val receivedProfileResetPinProfileId = args.internalProfileId
+ val receivedProfileResetPinIsAdmin = args.isAdmin
+
+ assertThat(receivedProfileResetPinProfileId).isEqualTo(0)
+ assertThat(receivedProfileResetPinIsAdmin).isEqualTo(false)
+ }
+ }
+ }
+
// TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them.
@Singleton
@Component(
diff --git a/app/src/sharedTest/java/org/oppia/android/app/spotlight/SpotlightFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/spotlight/SpotlightFragmentTest.kt
index 1e37adc5e68..9fb017111e3 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/spotlight/SpotlightFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/spotlight/SpotlightFragmentTest.kt
@@ -14,6 +14,7 @@ import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
import dagger.Component
import org.junit.After
import org.junit.Before
@@ -93,6 +94,7 @@ import org.oppia.android.util.networking.NetworkConnectionDebugUtilModule
import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule
import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule
import org.oppia.android.util.parser.image.ImageParsingModule
+import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
import javax.inject.Inject
@@ -339,6 +341,25 @@ class SpotlightFragmentTest {
}
}
+ @Test
+ fun testFragment_fragmentLoaded_verifyCorrectArgumentsPassed() {
+ TestPlatformParameterModule.forceEnableSpotlightUi(true)
+ launch(
+ createSpotlightFragmentTestActivity(context)
+ ).use { scenario ->
+ testCoroutineDispatchers.runCurrent()
+
+ scenario.onActivity { activity ->
+ val spotlightFragment = activity.supportFragmentManager
+ .findFragmentByTag(SpotlightManager.SPOTLIGHT_FRAGMENT_TAG) as SpotlightFragment
+ val receivedInternalProfileId = spotlightFragment
+ .arguments?.extractCurrentUserProfileId()?.internalId ?: -1
+
+ assertThat(receivedInternalProfileId).isEqualTo(0)
+ }
+ }
+ }
+
private fun setUpTestApplicationComponent() {
ApplicationProvider.getApplicationContext().inject(this)
}
diff --git a/app/src/sharedTest/java/org/oppia/android/app/story/StoryFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/story/StoryFragmentTest.kt
index c93e0f8460a..c8013552937 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/story/StoryFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/story/StoryFragmentTest.kt
@@ -67,6 +67,7 @@ import org.oppia.android.app.customview.LessonThumbnailImageView
import org.oppia.android.app.devoptions.DeveloperOptionsModule
import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule
import org.oppia.android.app.model.ProfileId
+import org.oppia.android.app.model.StoryFragmentArguments
import org.oppia.android.app.player.exploration.ExplorationActivity
import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule
import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPosition
@@ -132,6 +133,7 @@ import org.oppia.android.util.accessibility.AccessibilityTestModule
import org.oppia.android.util.accessibility.FakeAccessibilityService
import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
+import org.oppia.android.util.extensions.getProto
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.locale.LocaleProdModule
import org.oppia.android.util.logging.EventLoggingConfigurationModule
@@ -144,6 +146,7 @@ import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule
import org.oppia.android.util.parser.image.ImageLoader
import org.oppia.android.util.parser.image.ImageParsingModule
import org.oppia.android.util.parser.image.ImageTransformation
+import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
import javax.inject.Inject
@@ -772,6 +775,45 @@ class StoryFragmentTest {
}
}
+ @Test
+ fun testFragment_fragmentLoaded_verifyCorrectArgumentsPassed() {
+ launch(createFractionsStoryActivityIntent()).use { scenario ->
+ testCoroutineDispatchers.runCurrent()
+
+ scenario.onActivity { activity ->
+ val storyFragment = activity.supportFragmentManager
+ .findFragmentById(R.id.story_fragment_placeholder) as StoryFragment
+
+ val arguments = checkNotNull(storyFragment.arguments) {
+ "Expected arguments to be passed to StoryFragment."
+ }
+ val args = arguments.getProto(
+ StoryFragment.STORY_FRAGMENT_ARGUMENTS_KEY,
+ StoryFragmentArguments.getDefaultInstance()
+ )
+
+ val receivedInternalProfileId = arguments.extractCurrentUserProfileId().internalId
+ val receivedClassroomId =
+ checkNotNull(args.classroomId) {
+ "Expected classroomId to be passed to StoryFragment."
+ }
+ val receivedTopicId =
+ checkNotNull(args.topicId) {
+ "Expected topicId to be passed to StoryFragment."
+ }
+ val receivedStoryId =
+ checkNotNull(args.storyId) {
+ "Expected storyId to be passed to StoryFragment."
+ }
+
+ assertThat(receivedInternalProfileId).isEqualTo(internalProfileId)
+ assertThat(receivedClassroomId).isEqualTo(TEST_CLASSROOM_ID_1)
+ assertThat(receivedTopicId).isEqualTo(FRACTIONS_TOPIC_ID)
+ assertThat(receivedStoryId).isEqualTo(FRACTIONS_STORY_ID_0)
+ }
+ }
+ }
+
@Config(qualifiers = "+sw600dp")
@Test // TODO(#4212): Error -> No views in hierarchy found matching
fun testStoryFragment_completedChapter_checkProgressDrawableIsCorrect() {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/survey/SurveyFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/survey/SurveyFragmentTest.kt
index b600558c0da..332914c0c7c 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/survey/SurveyFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/survey/SurveyFragmentTest.kt
@@ -48,6 +48,7 @@ import org.oppia.android.app.devoptions.DeveloperOptionsModule
import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule
import org.oppia.android.app.model.ProfileId
import org.oppia.android.app.model.ScreenName
+import org.oppia.android.app.model.SurveyFragmentArguments
import org.oppia.android.app.model.SurveyQuestionName
import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule
import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView
@@ -101,6 +102,7 @@ import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
+import org.oppia.android.util.extensions.getProto
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.locale.LocaleProdModule
import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.extractCurrentAppScreenName
@@ -113,6 +115,7 @@ import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule
import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule
import org.oppia.android.util.parser.image.GlideImageLoaderModule
import org.oppia.android.util.parser.image.ImageParsingModule
+import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
import javax.inject.Inject
@@ -535,6 +538,32 @@ class SurveyFragmentTest {
}
}
+ @Test
+ fun testFragment_fragmentLoaded_verifyCorrectArgumentsPassed() {
+ launch(
+ createSurveyActivityIntent()
+ ).use { scenario ->
+ testCoroutineDispatchers.runCurrent()
+ scenario.onActivity { activity ->
+
+ val surveyFragment = activity.supportFragmentManager
+ .findFragmentById(R.id.survey_fragment_placeholder) as SurveyFragment
+ val args = surveyFragment.arguments!!.getProto(
+ SurveyFragment.SURVEY_FRAGMENT_ARGUMENTS_KEY,
+ SurveyFragmentArguments.getDefaultInstance()
+ )
+ val receivedInternalProfileId = surveyFragment.arguments!!
+ .extractCurrentUserProfileId().internalId
+ val receivedTopicId = args.topicId!!
+ val receivedExplorationId = args.explorationId!!
+
+ assertThat(receivedInternalProfileId).isEqualTo(0)
+ assertThat(receivedTopicId).isEqualTo(TEST_TOPIC_ID_0)
+ assertThat(receivedExplorationId).isEqualTo(TEST_EXPLORATION_ID_2)
+ }
+ }
+ }
+
private fun selectNpsAnswerAndMoveToNextQuestion(npsScore: Int) {
onView(
allOf(
diff --git a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListFragmentTest.kt
index 89403f4efe2..08c49683c59 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListFragmentTest.kt
@@ -18,6 +18,7 @@ import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
import dagger.Component
import org.hamcrest.Matchers.allOf
import org.junit.After
@@ -39,6 +40,8 @@ import org.oppia.android.app.devoptions.DeveloperOptionsModule
import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule
import org.oppia.android.app.help.thirdparty.LicenseListActivity
import org.oppia.android.app.help.thirdparty.ThirdPartyDependencyListActivity
+import org.oppia.android.app.help.thirdparty.ThirdPartyDependencyListFragment
+import org.oppia.android.app.model.ThirdPartyDependencyListFragmentArguments
import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule
import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPosition
import org.oppia.android.app.shim.ViewBindingShimModule
@@ -86,6 +89,7 @@ import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
+import org.oppia.android.util.extensions.getProto
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.locale.LocaleProdModule
import org.oppia.android.util.logging.EventLoggingConfigurationModule
@@ -447,6 +451,30 @@ class ThirdPartyDependencyListFragmentTest {
}
}
+ @Test
+ fun testFragment_fragmentLoaded_verifyCorrectArgumentsPassed() {
+ launch(ThirdPartyDependencyListActivity::class.java).use { scenario ->
+ testCoroutineDispatchers.runCurrent()
+ scenario.onActivity { activity ->
+
+ val thirdPartyDependencyListFragment = activity.supportFragmentManager
+ .findFragmentById(R.id.third_party_dependency_list_fragment_placeholder)
+ as ThirdPartyDependencyListFragment
+
+ val arguments = checkNotNull(thirdPartyDependencyListFragment.arguments) {
+ "Expected arguments to be passed to ThirdPartyDependencyListFragment"
+ }
+ val args = arguments.getProto(
+ "ThirdPartyDependencyListFragment.arguments",
+ ThirdPartyDependencyListFragmentArguments.getDefaultInstance()
+ )
+ val receivedIsMultipane = args?.isMultipane ?: false
+
+ assertThat(receivedIsMultipane).isEqualTo(false)
+ }
+ }
+ }
+
private fun retrieveDependencyName(id: Int): String {
return ApplicationProvider.getApplicationContext()
.resources.getString(id)
diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt
index 29dde65b725..8be20c6dc55 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt
@@ -26,6 +26,7 @@ import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
import dagger.Component
import dagger.Module
import dagger.Provides
@@ -58,6 +59,7 @@ import org.oppia.android.app.model.OptionsActivityParams
import org.oppia.android.app.model.ProfileId
import org.oppia.android.app.model.ReadingTextSize
import org.oppia.android.app.model.RevisionCardActivityParams
+import org.oppia.android.app.model.RevisionCardFragmentArguments
import org.oppia.android.app.model.WrittenTranslationLanguageSelection
import org.oppia.android.app.options.OptionsActivity
import org.oppia.android.app.player.exploration.ExplorationActivity
@@ -122,6 +124,7 @@ import org.oppia.android.util.accessibility.AccessibilityTestModule
import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.LoadImagesFromAssets
import org.oppia.android.util.caching.LoadLessonProtosFromAssets
+import org.oppia.android.util.extensions.getProto
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.locale.LocaleProdModule
import org.oppia.android.util.logging.EventLoggingConfigurationModule
@@ -133,6 +136,7 @@ import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule
import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule
import org.oppia.android.util.parser.image.GlideImageLoaderModule
import org.oppia.android.util.parser.image.ImageParsingModule
+import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
import javax.inject.Inject
@@ -810,6 +814,45 @@ class RevisionCardFragmentTest {
}
}
+ @Test
+ fun testFragment_fragmentLoaded_verifyCorrectArgumentsPassed() {
+ launch(
+ createRevisionCardActivityIntent(
+ context,
+ profileId.internalId,
+ FRACTIONS_TOPIC_ID,
+ subtopicId = 2,
+ FRACTIONS_SUBTOPIC_LIST_SIZE
+ )
+ ).use { scenario ->
+ testCoroutineDispatchers.runCurrent()
+ scenario.onActivity { activity ->
+
+ val revisionCardFragment = activity.supportFragmentManager
+ .findFragmentById(R.id.revision_card_fragment_placeholder) as RevisionCardFragment
+ val arguments = checkNotNull(revisionCardFragment.arguments) {
+ "Expected arguments to be passed to StoryFragment"
+ }
+ val args = arguments.getProto(
+ RevisionCardFragment.REVISION_CARD_FRAGMENT_ARGUMENTS_KEY,
+ RevisionCardFragmentArguments.getDefaultInstance()
+ )
+ val receivedTopicId =
+ checkNotNull(args?.topicId) {
+ "Expected topicId to be passed to RevisionCardFragment"
+ }
+ val receivedSubtopicId = args?.subtopicId ?: -1
+ val receivedProfileId = arguments.extractCurrentUserProfileId()
+ val receivedSubtopicListSize = args?.subtopicListSize ?: -1
+
+ assertThat(receivedTopicId).isEqualTo(FRACTIONS_TOPIC_ID)
+ assertThat(receivedSubtopicId).isEqualTo(2)
+ assertThat(receivedProfileId).isEqualTo(profileId)
+ assertThat(receivedSubtopicListSize).isEqualTo(FRACTIONS_SUBTOPIC_LIST_SIZE)
+ }
+ }
+ }
+
/** See the version in StateFragmentTest for documentation details. */
@Suppress("SameParameterValue")
private fun openClickableSpan(text: String): ViewAction {
From aadc1a1d944d92beed0c7532d2fc0d60b5644d37 Mon Sep 17 00:00:00 2001
From: "Mr. 17"
Date: Mon, 16 Sep 2024 17:40:16 +0530
Subject: [PATCH 5/5] Fix #5344: Remove temporary functions from
TopicListController (#5528)
## Explanation
Fixes #5344
This PR surfaces the `getClassrooms` and `getClassroomById` functions
from the `ClassroomController`, adds tests to ensure their correctness,
and refactors the `TopicListController` to replace temporary functions
with these new implementations.
## Essential Checklist
- [x] The PR title and explanation each start with "Fix #bugnum: " (If
this PR fixes part of an issue, prefix the title with "Fix part of
#bugnum: ...".)
- [x] Any changes to
[scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets)
files have their rationale included in the PR explanation.
- [x] The PR follows the [style
guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide).
- [x] The PR does not contain any unnecessary code changes from Android
Studio
([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)).
- [x] The PR is made from a branch that's **not** called "develop" and
is up-to-date with "develop".
- [x] The PR is **assigned** to the appropriate reviewers
([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)).
## For UI-specific PRs only
If your PR includes UI-related changes, then:
- Add screenshots for portrait/landscape for both a tablet & phone of
the before & after UI changes
- For the screenshots above, include both English and pseudo-localized
(RTL) screenshots (see [RTL
guide](https://github.com/oppia/oppia-android/wiki/RTL-Guidelines))
- Add a video showing the full UX flow with a screen reader enabled (see
[accessibility
guide](https://github.com/oppia/oppia-android/wiki/Accessibility-A11y-Guide))
- For PRs introducing new UI elements or color changes, both light and
dark mode screenshots must be included
- Add a screenshot demonstrating that you ran affected Espresso tests
locally & that they're passing
---
.../domain/classroom/ClassroomController.kt | 56 +++++---
.../domain/topic/TopicListController.kt | 133 +++---------------
.../classroom/ClassroomControllerTest.kt | 17 +++
3 files changed, 69 insertions(+), 137 deletions(-)
diff --git a/domain/src/main/java/org/oppia/android/domain/classroom/ClassroomController.kt b/domain/src/main/java/org/oppia/android/domain/classroom/ClassroomController.kt
index 20cc3ee832e..dc99f75c5ce 100644
--- a/domain/src/main/java/org/oppia/android/domain/classroom/ClassroomController.kt
+++ b/domain/src/main/java/org/oppia/android/domain/classroom/ClassroomController.kt
@@ -72,6 +72,32 @@ class ClassroomController @Inject constructor(
)
}
+ /**
+ * Returns the list of [ClassroomRecord]s currently available in the app.
+ */
+ fun getClassrooms(): List {
+ return if (loadLessonProtosFromAssets) {
+ assetRepository.loadProtoFromLocalAssets(
+ assetName = "classrooms",
+ baseMessage = ClassroomIdList.getDefaultInstance()
+ ).classroomIdsList.map { classroomId ->
+ getClassroomById(classroomId)
+ }
+ } else loadClassroomsFromJson()
+ }
+
+ /**
+ * Returns the [ClassroomRecord] associated with the given [classroomId].
+ */
+ fun getClassroomById(classroomId: String): ClassroomRecord {
+ return if (loadLessonProtosFromAssets) {
+ assetRepository.tryLoadProtoFromLocalAssets(
+ assetName = classroomId,
+ defaultMessage = ClassroomRecord.getDefaultInstance()
+ ) ?: ClassroomRecord.getDefaultInstance()
+ } else loadClassroomByIdFromJson(classroomId)
+ }
+
/**
* Returns the list of [TopicSummary]s currently tracked by the app, possibly up to
* [EVICTION_TIME_MILLIS] old.
@@ -90,7 +116,7 @@ class ClassroomController @Inject constructor(
*/
fun getClassroomIdByTopicId(topicId: String): String {
var classroomId = ""
- loadClassrooms().forEach {
+ getClassrooms().forEach {
if (it.topicPrerequisitesMap.keys.contains(topicId)) {
classroomId = it.id
}
@@ -333,17 +359,6 @@ class ClassroomController @Inject constructor(
.build()
}
- private fun loadClassrooms(): List {
- return if (loadLessonProtosFromAssets) {
- assetRepository.loadProtoFromLocalAssets(
- assetName = "classrooms",
- baseMessage = ClassroomIdList.getDefaultInstance()
- ).classroomIdsList.map { classroomId ->
- loadClassroomById(classroomId)
- }
- } else loadClassroomsFromJson()
- }
-
private fun loadClassroomsFromJson(): List {
// Load the classrooms.json file.
val classroomIdsObj = jsonAssetRetriever.loadJsonFromAsset("classrooms.json")
@@ -359,27 +374,20 @@ class ClassroomController @Inject constructor(
val classroomId = checkNotNull(classroomIds.optString(i)) {
"Expected non-null classroom ID at index $i."
}
- val classroomRecord = loadClassroomById(classroomId)
+ val classroomRecord = getClassroomById(classroomId)
classroomRecords.add(classroomRecord)
}
return classroomRecords
}
- private fun loadClassroomById(classroomId: String): ClassroomRecord {
- return if (loadLessonProtosFromAssets) {
- assetRepository.tryLoadProtoFromLocalAssets(
- assetName = classroomId,
- defaultMessage = ClassroomRecord.getDefaultInstance()
- ) ?: ClassroomRecord.getDefaultInstance()
- } else loadClassroomByIdFromJson(classroomId)
- }
-
private fun loadClassroomByIdFromJson(classroomId: String): ClassroomRecord {
// Load the classroom obj.
val classroomObj = jsonAssetRetriever.loadJsonFromAsset("$classroomId.json")
checkNotNull(classroomObj) { "Failed to load $classroomId.json." }
+ val classroomTitle = classroomObj.getJSONObject("classroom_title")
+
// Load the topic prerequisite map.
val topicPrereqsObj = checkNotNull(classroomObj.optJSONObject("topic_prerequisites")) {
"Expected classroom to have non-null topic_prerequisites."
@@ -398,6 +406,10 @@ class ClassroomController @Inject constructor(
id = checkNotNull(classroomObj.optString("classroom_id")) {
"Expected classroom to have ID."
}
+ translatableTitle = SubtitledHtml.newBuilder().apply {
+ contentId = classroomTitle.getStringFromObject("content_id")
+ html = classroomTitle.getStringFromObject("html")
+ }.build()
putAllTopicPrerequisites(
topicPrereqs.mapValues { (_, topicIds) ->
ClassroomRecord.TopicIdList.newBuilder().apply {
diff --git a/domain/src/main/java/org/oppia/android/domain/topic/TopicListController.kt b/domain/src/main/java/org/oppia/android/domain/topic/TopicListController.kt
index 4fa2ed9edef..b82f8f56f0f 100644
--- a/domain/src/main/java/org/oppia/android/domain/topic/TopicListController.kt
+++ b/domain/src/main/java/org/oppia/android/domain/topic/TopicListController.kt
@@ -5,9 +5,7 @@ import org.json.JSONObject
import org.oppia.android.app.model.ChapterPlayState
import org.oppia.android.app.model.ChapterProgress
import org.oppia.android.app.model.ChapterSummary
-import org.oppia.android.app.model.ClassroomIdList
import org.oppia.android.app.model.ClassroomRecord
-import org.oppia.android.app.model.ClassroomRecord.TopicIdList
import org.oppia.android.app.model.ComingSoonTopicList
import org.oppia.android.app.model.EphemeralTopicSummary
import org.oppia.android.app.model.LessonThumbnail
@@ -29,7 +27,7 @@ import org.oppia.android.app.model.TopicProgress
import org.oppia.android.app.model.TopicRecord
import org.oppia.android.app.model.TopicSummary
import org.oppia.android.app.model.UpcomingTopic
-import org.oppia.android.domain.classroom.TEST_CLASSROOM_ID_0
+import org.oppia.android.domain.classroom.ClassroomController
import org.oppia.android.domain.translation.TranslationController
import org.oppia.android.domain.util.JsonAssetRetriever
import org.oppia.android.domain.util.getStringFromObject
@@ -101,6 +99,7 @@ class TopicListController @Inject constructor(
private val oppiaClock: OppiaClock,
private val assetRepository: AssetRepository,
private val translationController: TranslationController,
+ private val classroomController: ClassroomController,
@LoadLessonProtosFromAssets private val loadLessonProtosFromAssets: Boolean
) {
@@ -137,7 +136,7 @@ class TopicListController @Inject constructor(
private fun createTopicList(contentLocale: OppiaLocale.ContentLocale): TopicList {
return if (loadLessonProtosFromAssets) {
- val topicIdList = loadCombinedClassroomsTopicIdList()
+ val topicIdList = loadCombinedTopicIdList()
return TopicList.newBuilder().apply {
// Only include topics currently playable in the topic list.
addAllTopicSummary(
@@ -152,7 +151,7 @@ class TopicListController @Inject constructor(
}
private fun loadTopicListFromJson(contentLocale: OppiaLocale.ContentLocale): TopicList {
- val topicIdList = loadCombinedClassroomsTopicIdList()
+ val topicIdList = loadCombinedTopicIdList()
val topicListBuilder = TopicList.newBuilder()
for (topicId in topicIdList) {
val ephemeralSummary = createEphemeralTopicSummary(topicId, contentLocale)
@@ -166,7 +165,7 @@ class TopicListController @Inject constructor(
}
private fun computeComingSoonTopicList(): ComingSoonTopicList {
- val topicIdList = loadCombinedClassroomsTopicIdList()
+ val topicIdList = loadCombinedTopicIdList()
val comingSoonTopicListBuilder = ComingSoonTopicList.newBuilder()
for (topicId in topicIdList) {
val upcomingTopicSummary = createUpcomingTopicSummary(topicId)
@@ -185,7 +184,7 @@ class TopicListController @Inject constructor(
contentLocale: OppiaLocale.ContentLocale
): EphemeralTopicSummary {
val topicSummary = createTopicSummary(topicId)
- val classroomRecord = loadClassroomById(topicSummary.classroomId)
+ val classroomRecord = classroomController.getClassroomById(topicSummary.classroomId)
return EphemeralTopicSummary.newBuilder().apply {
this.topicSummary = topicSummary
writtenTranslationContext =
@@ -217,7 +216,7 @@ class TopicListController @Inject constructor(
this.topicId = topicId
putAllWrittenTranslations(topicRecord.writtenTranslationsMap)
title = topicRecord.translatableTitle
- classroomId = getClassroomIdByTopicId(topicId)
+ classroomId = classroomController.getClassroomIdByTopicId(topicId)
totalChapterCount = storyRecords.map { it.chaptersList.size }.sum()
topicThumbnail = topicRecord.topicThumbnail
topicPlayAvailability = if (topicRecord.isPublished) {
@@ -259,7 +258,7 @@ class TopicListController @Inject constructor(
contentId = "title"
html = jsonObject.getStringFromObject("topic_name")
}.build()
- val classroomId = getClassroomIdByTopicId(topicId)
+ val classroomId = classroomController.getClassroomIdByTopicId(topicId)
// No written translations are included since none are retrieved from JSON.
return TopicSummary.newBuilder()
.setTopicId(topicId)
@@ -296,7 +295,7 @@ class TopicListController @Inject constructor(
html = jsonObject.getStringFromObject("topic_name")
}.build()
- val classroomId = getClassroomIdByTopicId(topicId)
+ val classroomId = classroomController.getClassroomIdByTopicId(topicId)
val classroomJsonObject = jsonAssetRetriever.loadJsonFromAsset("$classroomId.json")!!
val classroomTitle = classroomJsonObject.getJSONObject("classroom_title").let {
@@ -369,8 +368,8 @@ class TopicListController @Inject constructor(
sortedTopicProgressList.forEach { topicProgress ->
val topic = topicController.retrieveTopic(topicProgress.topicId)
val classroom = topic?.topicId?.let { topicId ->
- val classroomId = getClassroomIdByTopicId(topicId)
- loadClassroomById(classroomId)
+ val classroomId = classroomController.getClassroomIdByTopicId(topicId)
+ classroomController.getClassroomById(classroomId)
} ?: ClassroomRecord.getDefaultInstance()
// Ignore topics that are no longer on the device, or that have been unpublished.
if (topic?.topicPlayAvailability?.availabilityCase == AVAILABLE_TO_PLAY_NOW) {
@@ -556,7 +555,7 @@ class TopicListController @Inject constructor(
* being suggested.
*/
private fun retrieveTopicDependencies(topicId: String): List {
- val classrooms = loadClassrooms()
+ val classrooms = classroomController.getClassrooms()
for (classroom in classrooms) {
if (classroom.topicPrerequisitesMap.containsKey(topicId)) {
return classroom.topicPrerequisitesMap.getValue(topicId).topicIdsList
@@ -589,7 +588,7 @@ class TopicListController @Inject constructor(
contentLocale: OppiaLocale.ContentLocale
): List {
return if (loadLessonProtosFromAssets) {
- val topicIdList = loadCombinedClassroomsTopicIdList()
+ val topicIdList = loadCombinedTopicIdList()
return computeSuggestedStoriesForTopicIds(topicProgressList, topicIdList, contentLocale)
} else computeSuggestedStoriesFromJson(topicProgressList, contentLocale)
}
@@ -599,7 +598,7 @@ class TopicListController @Inject constructor(
contentLocale: OppiaLocale.ContentLocale
): List {
// All topics that could potentially be recommended.
- val topicIdList = loadCombinedClassroomsTopicIdList()
+ val topicIdList = loadCombinedTopicIdList()
return computeSuggestedStoriesForTopicIds(topicProgressList, topicIdList, contentLocale)
}
@@ -713,7 +712,7 @@ class TopicListController @Inject constructor(
)
val classroomRecord =
assetRepository.loadProtoFromLocalAssets(
- assetName = getClassroomIdByTopicId(topicId),
+ assetName = classroomController.getClassroomIdByTopicId(topicId),
baseMessage = ClassroomRecord.getDefaultInstance()
)
return PromotedStory.newBuilder().apply {
@@ -783,7 +782,7 @@ class TopicListController @Inject constructor(
}.build()
} ?: SubtitledHtml.getDefaultInstance()
- val classroomId = getClassroomIdByTopicId(topicId)
+ val classroomId = classroomController.getClassroomIdByTopicId(topicId)
val classroomJson = jsonAssetRetriever.loadJsonFromAsset("$classroomId.json")
if (classroomJson!!.optString("classroom_title").isNullOrEmpty()) return null
@@ -867,104 +866,8 @@ class TopicListController @Inject constructor(
.build()
}
- private fun getClassroomIdByTopicId(topicId: String): String {
- var classroomId = TEST_CLASSROOM_ID_0
- loadClassrooms().forEach {
- if (it.topicPrerequisitesMap.keys.contains(topicId)) {
- classroomId = it.id
- }
- }
- return classroomId
- }
-
- // TODO(#5344): Remove this in favor of per-classroom data handling.
- private fun loadClassrooms(): List {
- return if (loadLessonProtosFromAssets) {
- assetRepository.loadProtoFromLocalAssets(
- assetName = "classrooms",
- baseMessage = ClassroomIdList.getDefaultInstance()
- ).classroomIdsList.map { classroomId ->
- loadClassroomById(classroomId)
- }
- } else loadClassroomsFromJson()
- }
-
- // TODO(#5344): Remove this in favor of per-classroom data handling.
- private fun loadClassroomsFromJson(): List {
- // Load the classrooms.json file.
- val classroomIdsObj = jsonAssetRetriever.loadJsonFromAsset("classrooms.json")
- checkNotNull(classroomIdsObj) { "Failed to load classrooms.json." }
- val classroomIds = classroomIdsObj.optJSONArray("classroom_id_list")
- checkNotNull(classroomIds) { "classrooms.json is missing classroom IDs." }
-
- // Initialize a list to store the [ClassroomRecord]s.
- val classroomRecords = mutableListOf()
-
- // Iterate over all classroomIds and load each classroom's JSON.
- for (i in 0 until classroomIds.length()) {
- val classroomId = checkNotNull(classroomIds.optString(i)) {
- "Expected non-null classroom ID at index $i."
- }
- val classroomRecord = loadClassroomById(classroomId)
- classroomRecords.add(classroomRecord)
- }
-
- return classroomRecords
- }
-
- // TODO(#5344): Move this to classroom controller.
- private fun loadClassroomById(classroomId: String): ClassroomRecord {
- return if (loadLessonProtosFromAssets) {
- assetRepository.tryLoadProtoFromLocalAssets(
- assetName = classroomId,
- defaultMessage = ClassroomRecord.getDefaultInstance()
- ) ?: ClassroomRecord.getDefaultInstance()
- } else loadClassroomByIdFromJson(classroomId)
- }
-
- // TODO(#5344): Remove this in favor of per-classroom data handling.
- private fun loadClassroomByIdFromJson(classroomId: String): ClassroomRecord {
- // Load the classroom obj.
- val classroomObj = jsonAssetRetriever.loadJsonFromAsset("$classroomId.json")
- checkNotNull(classroomObj) { "Failed to load $classroomId.json." }
-
- val classroomTitle = classroomObj.getJSONObject("classroom_title")
-
- // Load the topic prerequisite map.
- val topicPrereqsObj = checkNotNull(classroomObj.optJSONObject("topic_prerequisites")) {
- "Expected classroom to have non-null topic_prerequisites."
- }
- val topicPrereqs = topicPrereqsObj.keys().asSequence().associateWith { topicId ->
- val topicIdArray = checkNotNull(topicPrereqsObj.optJSONArray(topicId)) {
- "Expected topic $topicId to have a non-null string list."
- }
- return@associateWith List(topicIdArray.length()) { index ->
- checkNotNull(topicIdArray.optString(index)) {
- "Expected topic $topicId to have non-null string at index $index."
- }
- }
- }
- return ClassroomRecord.newBuilder().apply {
- id = checkNotNull(classroomObj.optString("classroom_id")) {
- "Expected classroom to have ID."
- }
- translatableTitle = SubtitledHtml.newBuilder().apply {
- contentId = classroomTitle.getStringFromObject("content_id")
- html = classroomTitle.getStringFromObject("html")
- }.build()
- putAllTopicPrerequisites(
- topicPrereqs.mapValues { (_, topicIds) ->
- TopicIdList.newBuilder().apply {
- addAllTopicIds(topicIds)
- }.build()
- }
- )
- }.build()
- }
-
- // TODO(#5344): Remove this in favor of per-classroom data handling.
- private fun loadCombinedClassroomsTopicIdList(): List =
- loadClassrooms().flatMap { it.topicPrerequisitesMap.keys.toList() }
+ private fun loadCombinedTopicIdList(): List =
+ classroomController.getClassrooms().flatMap { it.topicPrerequisitesMap.keys.toList() }
}
internal fun createTopicThumbnailFromJson(topicJsonObject: JSONObject): LessonThumbnail {
diff --git a/domain/src/test/java/org/oppia/android/domain/classroom/ClassroomControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/classroom/ClassroomControllerTest.kt
index 24129abc307..6c1cee650bf 100644
--- a/domain/src/test/java/org/oppia/android/domain/classroom/ClassroomControllerTest.kt
+++ b/domain/src/test/java/org/oppia/android/domain/classroom/ClassroomControllerTest.kt
@@ -131,6 +131,23 @@ class ClassroomControllerTest {
assertThat(classroomList.classroomSummaryList.size).isEqualTo(2)
}
+ @Test
+ fun testGetClassrooms_returnsAllClassrooms() {
+ val classrooms = classroomController.getClassrooms()
+
+ assertThat(classrooms[0].id).isEqualTo(TEST_CLASSROOM_ID_0)
+ assertThat(classrooms[1].id).isEqualTo(TEST_CLASSROOM_ID_1)
+ assertThat(classrooms[2].id).isEqualTo(TEST_CLASSROOM_ID_2)
+ }
+
+ @Test
+ fun testGetClassroomById_hasCorrectClassroomInfo() {
+ val classroom = classroomController.getClassroomById(TEST_CLASSROOM_ID_0)
+
+ assertThat(classroom.id).isEqualTo(TEST_CLASSROOM_ID_0)
+ assertThat(classroom.translatableTitle.html).isEqualTo("Science")
+ }
+
@Test
fun testRetrieveTopicList_isSuccessful() {
val topicListProvider = classroomController.getTopicList(profileId0, TEST_CLASSROOM_ID_0)