Skip to content

Commit

Permalink
Fix oppia#2756: clickable text summary for inactive cards
Browse files Browse the repository at this point in the history
  • Loading branch information
Yash Raj committed Mar 19, 2021
1 parent 6288611 commit 8fc2dac
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package org.oppia.android.app.story

import android.content.res.Resources
import android.graphics.Typeface
import android.text.SpannableString
import android.text.Spanned
import android.text.TextPaint
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.util.DisplayMetrics
import android.util.TypedValue
import android.view.LayoutInflater
Expand All @@ -12,7 +18,9 @@ import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSmoothScroller
import androidx.recyclerview.widget.RecyclerView
import org.oppia.android.R
import org.oppia.android.app.home.RouteToExplorationListener
import org.oppia.android.app.model.ChapterPlayState
import org.oppia.android.app.model.EventLog
import org.oppia.android.app.recyclerview.BindableAdapter
import org.oppia.android.app.story.storyitemviewmodel.StoryChapterSummaryViewModel
Expand Down Expand Up @@ -142,6 +150,35 @@ class StoryFragmentPresenter @Inject constructor(
).parseOppiaHtml(
storyItemViewModel.summary, binding.chapterSummary
)
if (storyItemViewModel.chapterSummary.chapterPlayState
== ChapterPlayState.NOT_PLAYABLE_MISSING_PREREQUISITES
) {
val missingPrerequisiteSummary = fragment.getString(
R.string.chapter_prerequisite_title_label,
storyItemViewModel.index.toString(),
storyItemViewModel.missingPrerequisiteChapter.name
)
val chapterLockedSpannable = SpannableString(missingPrerequisiteSummary)
val clickableSpan = object : ClickableSpan() {
override fun onClick(widget: View) {
smoothScrollToPosition(storyItemViewModel.index - 1)
}

override fun updateDrawState(ds: TextPaint) {
super.updateDrawState(ds)
ds.isUnderlineText = false
ds.typeface = Typeface.DEFAULT_BOLD
}
}
chapterLockedSpannable.setSpan(
clickableSpan,
9,
chapterLockedSpannable.length - 24,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
binding.htmlContent = chapterLockedSpannable
binding.chapterSummary.movementMethod = LinkMovementMethod.getInstance()
}
}
)
.build()
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/res/layout-land/story_chapter_view.xml
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,9 @@
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp"
android:fontFamily="sans-serif"
android:text="@{viewModel.chapterSummary.chapterPlayState != ChapterPlayState.NOT_PLAYABLE_MISSING_PREREQUISITES ? htmlContent : @string/chapter_prerequisite_title_label(viewModel.index, viewModel.missingPrerequisiteChapter.name)}"
android:text="@{htmlContent}"
android:textColor="@color/oppiaPrimaryText"
android:textColorLink="@color/colorPrimary"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/res/layout-sw600dp/story_chapter_view.xml
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,9 @@
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp"
android:fontFamily="sans-serif"
android:text="@{viewModel.chapterSummary.chapterPlayState != ChapterPlayState.NOT_PLAYABLE_MISSING_PREREQUISITES ? htmlContent : @string/chapter_prerequisite_title_label(viewModel.index, viewModel.missingPrerequisiteChapter.name)}"
android:text="@{htmlContent}"
android:textColor="@color/oppiaPrimaryText"
android:textColorLink="@color/colorPrimary"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/res/layout/story_chapter_view.xml
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,9 @@
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp"
android:fontFamily="sans-serif"
android:text="@{viewModel.chapterSummary.chapterPlayState != ChapterPlayState.NOT_PLAYABLE_MISSING_PREREQUISITES ? htmlContent : @string/chapter_prerequisite_title_label(viewModel.index, viewModel.missingPrerequisiteChapter.name)}"
android:text="@{htmlContent}"
android:textColor="@color/oppiaPrimaryText"
android:textColorLink="@color/colorPrimary"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
Expand Down
132 changes: 116 additions & 16 deletions app/src/sharedTest/java/org/oppia/android/app/story/StoryFragmentTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,25 @@ import android.app.Application
import android.content.Context
import android.content.Intent
import android.content.res.Resources
import android.text.SpannableString
import android.text.style.ClickableSpan
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ActivityScenario.launch
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions.scrollToPosition
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withId
Expand All @@ -26,6 +34,7 @@ import dagger.Component
import dagger.Module
import dagger.Provides
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.Matchers
import org.junit.After
import org.junit.Before
import org.junit.Rule
Expand All @@ -51,6 +60,7 @@ import org.oppia.android.app.customview.LessonThumbnailImageView
import org.oppia.android.app.model.ProfileId
import org.oppia.android.app.player.exploration.ExplorationActivity
import org.oppia.android.app.player.state.hintsandsolution.HintsAndSolutionConfigModule
import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPosition
import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView
import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.hasItemCount
import org.oppia.android.app.shim.ViewBindingShimModule
Expand Down Expand Up @@ -359,10 +369,9 @@ class StoryFragmentTest {
}

@Test
fun testStoryFragment_configChange_chapterMissingPrerequisiteThumbnailIsBlurred() {
fun testStoryFragment_chapterMissingPrerequisiteIsShownCorrectly() {
launch<StoryActivity>(createFractionsStoryActivityIntent()).use {
testCoroutineDispatchers.runCurrent()
onView(isRoot()).perform(orientationLandscape())
onView(allOf(withId(R.id.story_chapter_list))).perform(
scrollToPosition<RecyclerView.ViewHolder>(
2
Expand All @@ -372,26 +381,21 @@ class StoryFragmentTest {
atPositionOnView(
recyclerViewId = R.id.story_chapter_list,
position = 2,
targetViewId = R.id.chapter_thumbnail
)
).check { view, noViewFoundException ->
var lessonThumbnailImageView = view.findViewById<LessonThumbnailImageView>(
R.id.chapter_thumbnail
targetViewId = R.id.chapter_summary
)
verify(lessonThumbnailImageView.imageLoader, atLeastOnce()).loadDrawable(
imageDrawableResId = anyInt(),
target = anyOrNull(),
transformations = capture(listCaptor)
).check(
matches(
withText("Complete Chapter 1: What is a Fraction? to unlock this chapter.")
)
assertThat(listCaptor.value).contains(ImageTransformation.BLUR)
}
)
}
}

@Test
fun testStoryFragment_chapterMissingPrerequisiteIsShownCorrectly() {
fun testStoryFragment_changeConfiguration_chapterMissingPrerequisiteIsShownCorrectly() {
launch<StoryActivity>(createFractionsStoryActivityIntent()).use {
testCoroutineDispatchers.runCurrent()
onView(isRoot()).perform(orientationLandscape())
onView(allOf(withId(R.id.story_chapter_list))).perform(
scrollToPosition<RecyclerView.ViewHolder>(
2
Expand All @@ -412,7 +416,40 @@ class StoryFragmentTest {
}

@Test
fun testStoryFragment_changeConfiguration_chapterMissingPrerequisiteIsShownCorrectly() {
fun testStoryFragment_clickPrerequisiteChapter_prerequisiteChapterCardIsDisplayed() {
launch<StoryActivity>(createFractionsStoryActivityIntent()).use {
testCoroutineDispatchers.runCurrent()
onView(allOf(withId(R.id.story_chapter_list))).perform(
scrollToPosition<RecyclerView.ViewHolder>(
2
)
)
onView(
atPositionOnView(
recyclerViewId = R.id.story_chapter_list,
position = 2,
targetViewId = R.id.chapter_summary
)
).perform(
clickClickableSpan(
"Complete Chapter 1: What is a Fraction? to unlock this chapter."
)
)
onView(
atPosition(
recyclerViewId = R.id.story_chapter_list,
position = 1
)
).check(
matches(
isDisplayed()
)
)
}
}

@Test
fun testStoryFragment_configChange_clickPrerequisiteChapter_prerequisiteChapterCardIsDisplayed() {
launch<StoryActivity>(createFractionsStoryActivityIntent()).use {
testCoroutineDispatchers.runCurrent()
onView(isRoot()).perform(orientationLandscape())
Expand All @@ -427,9 +464,19 @@ class StoryFragmentTest {
position = 2,
targetViewId = R.id.chapter_summary
)
).perform(
clickClickableSpan(
"Complete Chapter 1: What is a Fraction? to unlock this chapter."
)
)
onView(
atPosition(
recyclerViewId = R.id.story_chapter_list,
position = 1
)
).check(
matches(
withText("Complete Chapter 1: What is a Fraction? to unlock this chapter.")
isDisplayed()
)
)
}
Expand Down Expand Up @@ -489,6 +536,59 @@ class StoryFragmentTest {
}
}

/**
* Reference:
* https://stackoverflow.com/questions/38314077/how-to-click-a-clickablespan-using-espresso
*/
private fun clickClickableSpan(textToClick: CharSequence): ViewAction {
return object : ViewAction {

override fun getConstraints(): org.hamcrest.Matcher<View> {
return Matchers.instanceOf(TextView::class.java)
}

override fun getDescription(): String {
return "clicking on a ClickableSpan"
}

override fun perform(uiController: UiController, view: View) {
val textView = view as TextView
val spannableString = textView.text as SpannableString
if (spannableString.isEmpty()) {
// TextView is empty, nothing to do
throw NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build()
}
// Get the links inside the TextView and check if we find textToClick
val spans = spannableString.getSpans(
0,
spannableString.length,
ClickableSpan::class.java
)
if (spans.isNotEmpty()) {
var spanCandidate: ClickableSpan
for (span: ClickableSpan in spans) {
spanCandidate = span
val start = spannableString.getSpanStart(spanCandidate)
val end = spannableString.getSpanEnd(spanCandidate)
val sequence = spannableString.subSequence(start, end)
if (textToClick.toString().contains(sequence.toString())) {
span.onClick(textView)
return
}
}
}
// textToClick not found in TextView
throw NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build()
}
}
}

private fun createFractionsStoryActivityIntent(): Intent {
return StoryActivity.createStoryActivityIntent(
ApplicationProvider.getApplicationContext(),
Expand Down

0 comments on commit 8fc2dac

Please sign in to comment.