Skip to content

Commit

Permalink
Fix #117: Introduce topic list controller interface [Blocked: #175] (#…
Browse files Browse the repository at this point in the history
…176)

* Introduce initially viable TopicListController interface with tests, and
no real implementation.

This commit includes thumbnail structures that should be split into its
own branch/PR since other controllers will depend on it.

* Introduce 6 sample thumbnail graphics locally and a proto data structure
for thumbnails that can be used for lessons.

* Update topic list controller to use new-and-improved thumbnail options.

* Fix post-merge breakages.

* Address review comment.
  • Loading branch information
BenHenning authored Sep 25, 2019
1 parent 4968ea4 commit 4867dd9
Show file tree
Hide file tree
Showing 3 changed files with 451 additions and 0 deletions.
117 changes: 117 additions & 0 deletions domain/src/main/java/org/oppia/domain/topic/TopicListController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package org.oppia.domain.topic

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
import org.oppia.app.model.LessonThumbnail
import org.oppia.app.model.LessonThumbnailGraphic
import org.oppia.app.model.OngoingStoryList
import org.oppia.app.model.PromotedStory
import org.oppia.app.model.TopicList
import org.oppia.app.model.TopicSummary
import org.oppia.util.data.AsyncResult

const val TEST_TOPIC_ID_0 = "test_topic_id_0"
const val TEST_TOPIC_ID_1 = "test_topic_id_1"

private val EVICTION_TIME_MILLIS = TimeUnit.DAYS.toMillis(1)

/** Controller for retrieving the list of topics available to the learner to play. */
@Singleton
class TopicListController @Inject constructor() {
/**
* Returns the list of [TopicSummary]s currently tracked by the app, possibly up to
* [EVICTION_TIME_MILLIS] old.
*/
fun getTopicList(): LiveData<AsyncResult<TopicList>> {
return MutableLiveData(AsyncResult.success(createTopicList()))
}

/**
* Returns the list of ongoing [PromotedStory]s that can be viewed via a link on the homescreen. The total number of
* promoted stories should correspond to the ongoing story count within the [TopicList] returned by [getTopicList].
*/
fun getOngoingStoryList(): LiveData<AsyncResult<OngoingStoryList>> {
return MutableLiveData(AsyncResult.success(createOngoingStoryList()))
}

private fun createTopicList(): TopicList {
return TopicList.newBuilder()
.setPromotedStory(createPromotedStory1())
.addTopicSummary(createTopicSummary0())
.addTopicSummary(createTopicSummary1())
.setOngoingStoryCount(2)
.build()
}

private fun createTopicSummary0(): TopicSummary {
return TopicSummary.newBuilder()
.setTopicId(TEST_TOPIC_ID_0)
.setName("First Topic")
.setVersion(1)
.setSubtopicCount(0)
.setCanonicalStoryCount(2)
.setUncategorizedSkillCount(0)
.setAdditionalStoryCount(0)
.setTotalSkillCount(2)
.setTotalChapterCount(4)
.setTopicThumbnail(createTopicThumbnail0())
.build()
}

private fun createTopicSummary1(): TopicSummary {
return TopicSummary.newBuilder()
.setTopicId(TEST_TOPIC_ID_1)
.setName("Second Topic")
.setVersion(3)
.setSubtopicCount(0)
.setCanonicalStoryCount(1)
.setUncategorizedSkillCount(0)
.setAdditionalStoryCount(0)
.setTotalSkillCount(1)
.setTotalChapterCount(1)
.setTopicThumbnail(createTopicThumbnail1())
.build()
}

private fun createOngoingStoryList(): OngoingStoryList {
return OngoingStoryList.newBuilder()
.addRecentStory(createPromotedStory1())
.build()
}

private fun createPromotedStory1(): PromotedStory {
return PromotedStory.newBuilder()
.setStoryId(TEST_STORY_ID_1)
.setStoryName("Second Story")
.setTopicId(TEST_TOPIC_ID_0)
.setTopicName("First Topic")
.setCompletedChapterCount(1)
.setTotalChapterCount(3)
.setLessonThumbnail(createStoryThumbnail())
.build()
}

private fun createTopicThumbnail0(): LessonThumbnail {
return LessonThumbnail.newBuilder()
.setThumbnailGraphic(LessonThumbnailGraphic.CHILD_WITH_BOOK)
.setBackgroundColorRgb(0xd5836f)
.build()
}

private fun createTopicThumbnail1(): LessonThumbnail {
return LessonThumbnail.newBuilder()
.setThumbnailGraphic(LessonThumbnailGraphic.CHILD_WITH_CUPCAKES)
.setBackgroundColorRgb(0xf7bf73)
.build()
}

private fun createStoryThumbnail(): LessonThumbnail {
return LessonThumbnail.newBuilder()
.setThumbnailGraphic(LessonThumbnailGraphic.DUCK_AND_CHICKEN)
.setBackgroundColorRgb(0xa5d3ec)
.build()
}
}
242 changes: 242 additions & 0 deletions domain/src/test/java/org/oppia/domain/topic/TopicListControllerTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
package org.oppia.domain.topic

import android.app.Application
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import dagger.BindsInstance
import dagger.Component
import dagger.Module
import dagger.Provides
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.oppia.app.model.LessonThumbnailGraphic
import org.robolectric.annotation.Config
import javax.inject.Inject
import javax.inject.Singleton

/** Tests for [TopicListController]. */
@RunWith(AndroidJUnit4::class)
@Config(manifest = Config.NONE)
class TopicListControllerTest {
@Inject
lateinit var topicListController: TopicListController

@Before
fun setUp() {
setUpTestApplicationComponent()
}

// TODO(#15): Add tests for recommended lessons rather than promoted, and tests for the 'continue playing' LiveData
// not providing any data for cases when there are no ongoing lessons. Also, add tests for other uncovered cases
// (such as having and not having lessons in either of the OngoingStoryList section, or AsyncResult errors).

@Test
fun testRetrieveTopicList_isSuccessful() {
val topicListLiveData = topicListController.getTopicList()

val topicListResult = topicListLiveData.value
assertThat(topicListResult).isNotNull()
assertThat(topicListResult!!.isSuccess()).isTrue()
}

@Test
fun testRetrieveTopicList_providesListOfMultipleTopics() {
val topicListLiveData = topicListController.getTopicList()

val topicList = topicListLiveData.value!!.getOrThrow()
assertThat(topicList.topicSummaryCount).isGreaterThan(1)
}

@Test
fun testRetrieveTopicList_firstTopic_hasCorrectTopicInfo() {
val topicListLiveData = topicListController.getTopicList()

val topicList = topicListLiveData.value!!.getOrThrow()
val firstTopic = topicList.getTopicSummary(0)
assertThat(firstTopic.topicId).isEqualTo(TEST_TOPIC_ID_0)
assertThat(firstTopic.name).isEqualTo("First Topic")
}

@Test
fun testRetrieveTopicList_firstTopic_hasCorrectThumbnail() {
val topicListLiveData = topicListController.getTopicList()

val topicList = topicListLiveData.value!!.getOrThrow()
val firstTopic = topicList.getTopicSummary(0)
assertThat(firstTopic.topicThumbnail.thumbnailGraphic).isEqualTo(LessonThumbnailGraphic.CHILD_WITH_BOOK)
}

@Test
fun testRetrieveTopicList_firstTopic_hasCorrectLessonCount() {
val topicListLiveData = topicListController.getTopicList()

val topicList = topicListLiveData.value!!.getOrThrow()
val firstTopic = topicList.getTopicSummary(0)
assertThat(firstTopic.totalChapterCount).isEqualTo(4)
}

@Test
fun testRetrieveTopicList_secondTopic_hasCorrectTopicInfo() {
val topicListLiveData = topicListController.getTopicList()

val topicList = topicListLiveData.value!!.getOrThrow()
val secondTopic = topicList.getTopicSummary(1)
assertThat(secondTopic.topicId).isEqualTo(TEST_TOPIC_ID_1)
assertThat(secondTopic.name).isEqualTo("Second Topic")
}

@Test
fun testRetrieveTopicList_secondTopic_hasCorrectThumbnail() {
val topicListLiveData = topicListController.getTopicList()

val topicList = topicListLiveData.value!!.getOrThrow()
val secondTopic = topicList.getTopicSummary(1)
assertThat(secondTopic.topicThumbnail.thumbnailGraphic).isEqualTo(LessonThumbnailGraphic.CHILD_WITH_CUPCAKES)
}

@Test
fun testRetrieveTopicList_secondTopic_hasCorrectLessonCount() {
val topicListLiveData = topicListController.getTopicList()

val topicList = topicListLiveData.value!!.getOrThrow()
val secondTopic = topicList.getTopicSummary(1)
assertThat(secondTopic.totalChapterCount).isEqualTo(1)
}

@Test
fun testRetrieveTopicList_promotedLesson_hasCorrectLessonInfo() {
val topicListLiveData = topicListController.getTopicList()

val topicList = topicListLiveData.value!!.getOrThrow()
val promotedStory = topicList.promotedStory
assertThat(promotedStory.storyId).isEqualTo(TEST_STORY_ID_1)
assertThat(promotedStory.storyName).isEqualTo("Second Story")
}

@Test
fun testRetrieveTopicList_promotedLesson_hasCorrectTopicInfo() {
val topicListLiveData = topicListController.getTopicList()

val topicList = topicListLiveData.value!!.getOrThrow()
val promotedStory = topicList.promotedStory
assertThat(promotedStory.topicId).isEqualTo(TEST_TOPIC_ID_0)
assertThat(promotedStory.topicName).isEqualTo("First Topic")
}

@Test
fun testRetrieveTopicList_promotedLesson_hasCorrectCompletionStats() {
val topicListLiveData = topicListController.getTopicList()

val topicList = topicListLiveData.value!!.getOrThrow()
val promotedStory = topicList.promotedStory
assertThat(promotedStory.completedChapterCount).isEqualTo(1)
assertThat(promotedStory.totalChapterCount).isEqualTo(3)
}

@Test
fun testRetrieveTopicList_hasMultipleOngoingLessons() {
val topicListLiveData = topicListController.getTopicList()

val topicList = topicListLiveData.value!!.getOrThrow()
assertThat(topicList.ongoingStoryCount).isEqualTo(2)
}

@Test
fun testRetrieveOngoingStoryList_isSuccessful() {
val ongoingStoryListLiveData = topicListController.getOngoingStoryList()

val ongoingStoryListResult = ongoingStoryListLiveData.value
assertThat(ongoingStoryListResult).isNotNull()
assertThat(ongoingStoryListResult!!.isSuccess()).isTrue()
}

@Test
fun testRetrieveOngoingStoryList_withinSevenDays_hasOngoingLesson() {
val ongoingStoryListLiveData = topicListController.getOngoingStoryList()

val ongoingStoryList = ongoingStoryListLiveData.value!!.getOrThrow()
assertThat(ongoingStoryList.recentStoryCount).isEqualTo(1)
}

@Test
fun testRetrieveOngoingStoryList_recentLesson_hasCorrectStoryInfo() {
val ongoingStoryListLiveData = topicListController.getOngoingStoryList()

val ongoingStoryList = ongoingStoryListLiveData.value!!.getOrThrow()
val recentLesson = ongoingStoryList.getRecentStory(0)
assertThat(recentLesson.storyId).isEqualTo(TEST_STORY_ID_1)
assertThat(recentLesson.storyName).isEqualTo("Second Story")
}

@Test
fun testRetrieveOngoingStoryList_recentLesson_hasCorrectTopicInfo() {
val ongoingStoryListLiveData = topicListController.getOngoingStoryList()

val ongoingStoryList = ongoingStoryListLiveData.value!!.getOrThrow()
val recentLesson = ongoingStoryList.getRecentStory(0)
assertThat(recentLesson.topicId).isEqualTo(TEST_TOPIC_ID_0)
assertThat(recentLesson.topicName).isEqualTo("First Topic")
}

@Test
fun testRetrieveOngoingStoryList_recentLesson_hasCorrectCompletionStats() {
val ongoingStoryListLiveData = topicListController.getOngoingStoryList()

val ongoingStoryList = ongoingStoryListLiveData.value!!.getOrThrow()
val recentLesson = ongoingStoryList.getRecentStory(0)
assertThat(recentLesson.completedChapterCount).isEqualTo(1)
assertThat(recentLesson.totalChapterCount).isEqualTo(3)
}

@Test
fun testRetrieveOngoingStoryList_recentLesson_hasCorrectThumbnail() {
val ongoingStoryListLiveData = topicListController.getOngoingStoryList()

val ongoingStoryList = ongoingStoryListLiveData.value!!.getOrThrow()
val recentLesson = ongoingStoryList.getRecentStory(0)
assertThat(recentLesson.lessonThumbnail.thumbnailGraphic).isEqualTo(LessonThumbnailGraphic.DUCK_AND_CHICKEN)
}

@Test
fun testRetrieveOngoingStoryList_earlierThanSevenDays_doesNotHaveOngoingLesson() {
val ongoingStoryListLiveData = topicListController.getOngoingStoryList()

val ongoingStoryList = ongoingStoryListLiveData.value!!.getOrThrow()
assertThat(ongoingStoryList.olderStoryCount).isEqualTo(0)
}

private fun setUpTestApplicationComponent() {
DaggerTopicListControllerTest_TestApplicationComponent.builder()
.setApplication(ApplicationProvider.getApplicationContext())
.build()
.inject(this)
}

// TODO(#89): Move this to a common test application component.
@Module
class TestModule {
@Provides
@Singleton
fun provideContext(application: Application): Context {
return application
}
}

// TODO(#89): Move this to a common test application component.
@Singleton
@Component(modules = [TestModule::class])
interface TestApplicationComponent {
@Component.Builder
interface Builder {
@BindsInstance
fun setApplication(application: Application): Builder

fun build(): TestApplicationComponent
}

fun inject(topicListControllerTest: TopicListControllerTest)
}
}
Loading

0 comments on commit 4867dd9

Please sign in to comment.